From 20bda01e37087b103c036cad152e651356e91145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Franti=C5=A1ek=20Bu=C4=8D=C3=ADk?= <bucik@ics.muni.cz> Date: Mon, 8 Aug 2022 14:58:58 +0200 Subject: [PATCH] refactor: Refactored how brokers are structured and configured Needs new configuration BRAKING CHANGE: Update configuration according to application.yml, dropped profiles --- pom.xml | 41 +- .../java/cz/muni/ics/ga4gh/Application.java | 1 + .../cz/muni/ics/ga4gh/ApplicationContext.java | 21 +- .../muni/ics/ga4gh/adapters/PerunAdapter.java | 39 -- .../ga4gh/adapters/PerunAdapterMethods.java | 23 - .../ga4gh/adapters/impl/PerunAdapterImpl.java | 72 --- .../ga4gh/adapters/impl/PerunAdapterLdap.java | 199 -------- .../ga4gh/adapters/impl/PerunAdapterRpc.java | 283 ----------- .../java/cz/muni/ics/ga4gh/aop/LogTimes.java | 11 - .../cz/muni/ics/ga4gh/base/AdapterBeans.java | 68 +++ .../keystore => base}/JWKSetKeyStore.java | 11 +- .../java/cz/muni/ics/ga4gh/base/Utils.java | 332 ++++++++++++ .../ics/ga4gh/base/adapters/PerunAdapter.java | 52 ++ .../base/adapters/PerunAdapterMethods.java | 28 + .../adapters/PerunAdapterMethodsLdap.java | 2 +- .../adapters/PerunAdapterMethodsRpc.java | 9 +- .../adapters/PerunRpcAdapterMapper.java} | 29 +- .../base/adapters/impl/PerunAdapterImpl.java | 128 +++++ .../base/adapters/impl/PerunAdapterLdap.java | 350 +++++++++++++ .../base/adapters/impl/PerunAdapterRpc.java | 480 ++++++++++++++++++ .../connectors/PerunConnectorLdap.java | 79 ++- .../connectors/PerunConnectorRpc.java | 145 +++--- .../ga4gh/{ => base}/enums/MemberStatus.java | 2 +- .../exceptions/ConfigurationException.java | 26 + .../InconvertibleValueException.java | 2 +- .../InvalidRequestParametersException.java | 26 + .../exceptions/MissingFieldException.java | 2 +- .../PerunAdapterOperationException.java | 26 + .../exceptions/UserNotFoundException.java | 26 + .../exceptions/UserNotUniqueException.java | 4 +- .../ga4gh/{ => base}/model/Affiliation.java | 7 +- .../{ => base}/model/AttributeMapping.java | 10 +- .../base/model/BasicAuthCredentials.java | 28 + .../model/ClaimRepositoryHeader.java} | 18 +- .../muni/ics/ga4gh/base/model/ExtSource.java | 28 + .../model/Ga4ghClaimRepository.java | 10 +- .../ics/ga4gh/base/model/Ga4ghPassport.java | 34 ++ .../ga4gh/base/model/Ga4ghPassportVisa.java | 119 +++++ .../ga4gh/base/model/Ga4ghPassportVisaV1.java | 31 ++ .../ics/ga4gh/{ => base}/model/Group.java | 18 +- .../ics/ga4gh/{ => base}/model/Member.java | 14 +- .../{ => base}/model/PerunAttributeValue.java | 19 +- .../ga4gh/{ => base}/model/UserExtSource.java | 22 +- .../AttributeMappingProperties.java | 38 ++ .../base/properties/BasicAuthProperties.java | 47 ++ .../properties/BrokerInstanceProperties.java | 89 ++++ .../properties/Ga4ghBrokersProperties.java | 75 +++ .../Ga4ghClaimRepositoryProperties.java | 32 ++ .../properties/PerunAdapterProperties.java | 45 ++ .../PerunLdapConnectorProperties.java | 86 ++++ .../PerunRpcConnectorProperties.java | 79 +++ .../muni/ics/ga4gh/config/AdapterConfig.java | 19 - .../ics/ga4gh/config/AttributesConfig.java | 20 - .../ics/ga4gh/config/BasicAuthConfig.java | 19 - .../muni/ics/ga4gh/config/BrokerConfig.java | 37 -- .../cz/muni/ics/ga4gh/config/Ga4ghConfig.java | 20 - .../cz/muni/ics/ga4gh/config/LdapConfig.java | 33 -- .../cz/muni/ics/ga4gh/config/RpcConfig.java | 25 - .../controllers/Ga4ghBrokerController.java | 35 -- .../ics/ga4gh/facade/Ga4ghBrokerFacade.java | 8 +- .../facade/impl/Ga4GhBrokerFacadeImpl.java | 33 +- .../cz/muni/ics/ga4gh/model/ExtSource.java | 40 -- .../ics/ga4gh/model/Ga4ghPassportVisa.java | 59 --- .../java/cz/muni/ics/ga4gh/model/Repo.java | 27 - .../ics/ga4gh/service/Ga4ghBrokerService.java | 9 +- .../service/Ga4ghPassportBrokerBeans.java | 56 ++ .../JWTSigningAndValidationService.java | 19 +- .../service/PassportAssemblyContext.java | 35 ++ .../impl/Ga4GhBrokerBrokerServiceImpl.java | 94 ++-- .../JWTSigningAndValidationServiceImpl.java | 134 ++--- .../service/impl/VisaAssemblyParameters.java | 37 ++ .../impl/brokers/BbmriGa4ghBroker.java | 331 ++++++++---- .../impl/brokers/ElixirGa4ghBroker.java | 326 ++++++++---- .../service/impl/brokers/Ga4ghBroker.java | 277 ++++++---- .../brokers/LifescienceRiGa4ghBroker.java | 330 ++++++++++++ .../impl/brokers/PerunGa4ghBroker.java | 154 ++++++ .../java/cz/muni/ics/ga4gh/utils/Utils.java | 218 -------- .../web/controllers/ExceptionTranslator.java | 29 ++ .../controllers/Ga4ghBrokerController.java | 39 ++ .../controllers/JwkSetPublishingEndpoint.java | 19 +- .../security/WebSecurityConfigurer.java | 30 +- src/main/resources/application-bbmri.yml | 77 --- src/main/resources/application-elixir.yml | 92 ---- src/main/resources/application.yml | 134 ++++- 84 files changed, 4107 insertions(+), 2074 deletions(-) delete mode 100644 src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapter.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethods.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterImpl.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterLdap.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterRpc.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/aop/LogTimes.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/AdapterBeans.java rename src/main/java/cz/muni/ics/ga4gh/{jose/keystore => base}/JWKSetKeyStore.java (98%) create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/Utils.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapter.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethods.java rename src/main/java/cz/muni/ics/ga4gh/{ => base}/adapters/PerunAdapterMethodsLdap.java (52%) rename src/main/java/cz/muni/ics/ga4gh/{ => base}/adapters/PerunAdapterMethodsRpc.java (54%) rename src/main/java/cz/muni/ics/ga4gh/{mappers/RpcMapper.java => base/adapters/PerunRpcAdapterMapper.java} (91%) create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterImpl.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterLdap.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterRpc.java rename src/main/java/cz/muni/ics/ga4gh/{ => base}/connectors/PerunConnectorLdap.java (66%) rename src/main/java/cz/muni/ics/ga4gh/{ => base}/connectors/PerunConnectorRpc.java (56%) rename src/main/java/cz/muni/ics/ga4gh/{ => base}/enums/MemberStatus.java (94%) create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/exceptions/ConfigurationException.java rename src/main/java/cz/muni/ics/ga4gh/{ => base}/exceptions/InconvertibleValueException.java (92%) create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/exceptions/InvalidRequestParametersException.java rename src/main/java/cz/muni/ics/ga4gh/{ => base}/exceptions/MissingFieldException.java (92%) create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/exceptions/PerunAdapterOperationException.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotFoundException.java rename src/main/java/cz/muni/ics/ga4gh/{ => base}/exceptions/UserNotUniqueException.java (82%) rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/Affiliation.java (63%) rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/AttributeMapping.java (66%) create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/model/BasicAuthCredentials.java rename src/main/java/cz/muni/ics/ga4gh/{model/RepoHeader.java => base/model/ClaimRepositoryHeader.java} (59%) create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/model/ExtSource.java rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/Ga4ghClaimRepository.java (60%) create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassport.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisa.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisaV1.java rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/Group.java (75%) rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/Member.java (56%) rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/PerunAttributeValue.java (98%) rename src/main/java/cz/muni/ics/ga4gh/{ => base}/model/UserExtSource.java (77%) create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/AttributeMappingProperties.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/BasicAuthProperties.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/BrokerInstanceProperties.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghBrokersProperties.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghClaimRepositoryProperties.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/PerunAdapterProperties.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/PerunLdapConnectorProperties.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/base/properties/PerunRpcConnectorProperties.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/AdapterConfig.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/AttributesConfig.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/BasicAuthConfig.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/BrokerConfig.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/Ga4ghConfig.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/LdapConfig.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/config/RpcConfig.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/controllers/Ga4ghBrokerController.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/model/ExtSource.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/model/Ga4ghPassportVisa.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/model/Repo.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/service/Ga4ghPassportBrokerBeans.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/service/PassportAssemblyContext.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/service/impl/VisaAssemblyParameters.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/LifescienceRiGa4ghBroker.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/PerunGa4ghBroker.java delete mode 100644 src/main/java/cz/muni/ics/ga4gh/utils/Utils.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/web/controllers/ExceptionTranslator.java create mode 100644 src/main/java/cz/muni/ics/ga4gh/web/controllers/Ga4ghBrokerController.java rename src/main/java/cz/muni/ics/ga4gh/{ => web}/controllers/JwkSetPublishingEndpoint.java (77%) rename src/main/java/cz/muni/ics/ga4gh/{ => web}/security/WebSecurityConfigurer.java (51%) delete mode 100644 src/main/resources/application-bbmri.yml delete mode 100644 src/main/resources/application-elixir.yml diff --git a/pom.xml b/pom.xml index 4544eee..f0c12fb 100644 --- a/pom.xml +++ b/pom.xml @@ -24,15 +24,15 @@ <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> - <version.springfox>3.0.0</version.springfox> - <version.swagger>3.0.0</version.swagger> - <version.apache-directory-api>2.1.0</version.apache-directory-api> - <version.nimbus-jose-jwt>9.23</version.nimbus-jose-jwt> - <version.guava>31.1-jre</version.guava> - <version.bouncy-castle>1.70</version.bouncy-castle> + <springfox.version>3.0.0</springfox.version> + <apache-directory-api.version>2.1.0</apache-directory-api.version> + <nimbus-jose-jwt.version>9.23</nimbus-jose-jwt.version> + <guava.version>31.1-jre</guava.version> + <bouncy-castle.version>1.70</bouncy-castle.version> </properties> <dependencies> + <!-- SPRING --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> @@ -55,51 +55,68 @@ <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.springframework.boot</groupId> + <artifactId>spring-boot-starter-validation</artifactId> + </dependency> + <!-- LOMBOK --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> + <!-- SPRINGFOX --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> - <version>${version.springfox}</version> + <version>${springfox.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> - <version>${version.swagger}</version> + <version>${springfox.version}</version> </dependency> + <!-- APACHE HTTPCLIENT --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> + + <!-- APACHE DIRECTORY --> <dependency> <groupId>org.apache.directory.api</groupId> <artifactId>api-all</artifactId> - <version>${version.apache-directory-api}</version> + <version>${apache-directory-api.version}</version> </dependency> + <!-- BOUNCYCASTLE --> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcpkix-jdk15on</artifactId> - <version>${version.bouncy-castle}</version> + <version>${bouncy-castle.version}</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> - <version>${version.bouncy-castle}</version> + <version>${bouncy-castle.version}</version> <scope>compile</scope> </dependency> + <!-- GUAVA --> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> - <version>${version.guava}</version> + <version>${guava.version}</version> + </dependency> + + <!-- HIBERNATE VALIDATOR --> + <dependency> + <groupId>org.hibernate.validator</groupId> + <artifactId>hibernate-validator</artifactId> </dependency> </dependencies> diff --git a/src/main/java/cz/muni/ics/ga4gh/Application.java b/src/main/java/cz/muni/ics/ga4gh/Application.java index efc5f3c..b729527 100644 --- a/src/main/java/cz/muni/ics/ga4gh/Application.java +++ b/src/main/java/cz/muni/ics/ga4gh/Application.java @@ -10,4 +10,5 @@ public class Application extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(Application.class, args); } + } diff --git a/src/main/java/cz/muni/ics/ga4gh/ApplicationContext.java b/src/main/java/cz/muni/ics/ga4gh/ApplicationContext.java index 13e6043..be93e28 100644 --- a/src/main/java/cz/muni/ics/ga4gh/ApplicationContext.java +++ b/src/main/java/cz/muni/ics/ga4gh/ApplicationContext.java @@ -1,28 +1,21 @@ package cz.muni.ics.ga4gh; -import cz.muni.ics.ga4gh.config.BrokerConfig; +import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.FileUrlResource; import org.springframework.core.io.Resource; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; -import java.net.MalformedURLException; - -@Configuration +@Component +@ConfigurationPropertiesScan public class ApplicationContext { - private final String pathToJwkFile; - - public ApplicationContext(BrokerConfig config) { - this.pathToJwkFile = config.getPathToJwkFile(); - } - @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) @@ -38,7 +31,7 @@ public class ApplicationContext { } @Bean - public Resource jwks() throws MalformedURLException { - return new FileUrlResource(pathToJwkFile); + public Resource jwkFileResource(Ga4ghBrokersProperties ga4ghBrokersProperties) { + return ga4ghBrokersProperties.getJwkKeystoreFile(); } } diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapter.java b/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapter.java deleted file mode 100644 index 7358f9a..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapter.java +++ /dev/null @@ -1,39 +0,0 @@ -package cz.muni.ics.ga4gh.adapters; - -import cz.muni.ics.ga4gh.adapters.impl.PerunAdapterLdap; -import cz.muni.ics.ga4gh.adapters.impl.PerunAdapterRpc; -import cz.muni.ics.ga4gh.config.AdapterConfig; -import lombok.Getter; -import lombok.Setter; - -import java.util.Objects; - -@Getter -@Setter -public abstract class PerunAdapter implements PerunAdapterMethods { - - private final String RPC = "rpc"; - - private PerunAdapterMethods adapterPrimary; - private PerunAdapterMethods adapterFallback; - - private PerunAdapterMethodsRpc adapterRpc; - private PerunAdapterMethodsLdap adapterLdap; - - private boolean callFallback; - - public PerunAdapter(AdapterConfig config, PerunAdapterRpc adapterRpc, PerunAdapterLdap adapterLdap) { - if (config.getAdapterPrimary() != null && config.getAdapterPrimary().equalsIgnoreCase(RPC)) { - this.adapterPrimary = adapterRpc; - this.adapterFallback = adapterLdap; - } else { - this.adapterPrimary = adapterLdap; - this.adapterFallback = adapterRpc; - } - - this.adapterRpc = adapterRpc; - this.adapterLdap = adapterLdap; - - this.callFallback = Objects.requireNonNullElse(config.getCallFallback(), false); - } -} diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethods.java b/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethods.java deleted file mode 100644 index 85485da..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethods.java +++ /dev/null @@ -1,23 +0,0 @@ -package cz.muni.ics.ga4gh.adapters; - -import cz.muni.ics.ga4gh.model.Affiliation; -import cz.muni.ics.ga4gh.model.AttributeMapping; - -import java.util.List; -import java.util.Set; - -public interface PerunAdapterMethods { - - /** - * Fetch user based on his principal (extLogin and extSource) from Perun - * - * @return PerunUser with id of found user - */ - Long getPreauthenticatedUserId(String extLogin, String extSourceName); - - boolean isUserInGroup(Long userId, Long groupId); - - List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr); - - Set<Long> getUserIdsByAttributeValue(AttributeMapping attrName, String attrValue); -} diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterImpl.java b/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterImpl.java deleted file mode 100644 index fd5553b..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterImpl.java +++ /dev/null @@ -1,72 +0,0 @@ -package cz.muni.ics.ga4gh.adapters.impl; - -import cz.muni.ics.ga4gh.adapters.PerunAdapter; -import cz.muni.ics.ga4gh.config.AdapterConfig; -import cz.muni.ics.ga4gh.model.Affiliation; -import cz.muni.ics.ga4gh.model.AttributeMapping; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.List; -import java.util.Set; - -@Component -public class PerunAdapterImpl extends PerunAdapter { - - @Autowired - public PerunAdapterImpl(AdapterConfig config, PerunAdapterRpc adapterRpc, PerunAdapterLdap adapterLdap) { - super(config, adapterRpc, adapterLdap); - } - - @Override - public Long getPreauthenticatedUserId(String extLogin, String extSourceName) { - try { - return this.getAdapterPrimary().getPreauthenticatedUserId(extLogin, extSourceName); - } catch (UnsupportedOperationException e) { - if (this.isCallFallback()) { - return this.getAdapterFallback().getPreauthenticatedUserId(extLogin, extSourceName); - } else { - throw e; - } - } - } - - @Override - public boolean isUserInGroup(Long userId, Long groupId) { - try { - return this.getAdapterPrimary().isUserInGroup(userId, groupId); - } catch (UnsupportedOperationException e) { - if (this.isCallFallback()) { - return this.getAdapterFallback().isUserInGroup(userId, groupId); - } else { - throw e; - } - } - } - - @Override - public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) { - try { - return this.getAdapterPrimary().getGroupAffiliations(userId, groupAffiliationsAttr); - } catch (UnsupportedOperationException e) { - if (this.isCallFallback()) { - return this.getAdapterFallback().getGroupAffiliations(userId, groupAffiliationsAttr); - } else { - throw e; - } - } - } - - @Override - public Set<Long> getUserIdsByAttributeValue(AttributeMapping attrName, String attrValue) { - try { - return this.getAdapterPrimary().getUserIdsByAttributeValue(attrName, attrValue); - } catch (UnsupportedOperationException e) { - if (this.isCallFallback()) { - return this.getAdapterFallback().getUserIdsByAttributeValue(attrName, attrValue); - } else { - throw e; - } - } - } -} diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterLdap.java b/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterLdap.java deleted file mode 100644 index 2c68d7e..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterLdap.java +++ /dev/null @@ -1,199 +0,0 @@ -package cz.muni.ics.ga4gh.adapters.impl; - -import cz.muni.ics.ga4gh.adapters.PerunAdapterMethods; -import cz.muni.ics.ga4gh.adapters.PerunAdapterMethodsLdap; -import cz.muni.ics.ga4gh.config.AttributesConfig; -import cz.muni.ics.ga4gh.connectors.PerunConnectorLdap; -import cz.muni.ics.ga4gh.model.Affiliation; -import cz.muni.ics.ga4gh.model.AttributeMapping; -import org.apache.directory.api.ldap.model.entry.Attribute; -import org.apache.directory.api.ldap.model.entry.Entry; -import org.apache.directory.api.ldap.model.message.SearchScope; -import org.apache.directory.ldap.client.api.search.FilterBuilder; -import org.apache.directory.ldap.client.template.EntryMapper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import static org.apache.directory.ldap.client.api.search.FilterBuilder.and; -import static org.apache.directory.ldap.client.api.search.FilterBuilder.equal; -import static org.apache.directory.ldap.client.api.search.FilterBuilder.or; - -@Component -public class PerunAdapterLdap implements PerunAdapterMethods, PerunAdapterMethodsLdap { - - public static final String OBJECT_CLASS = "objectClass"; - public static final String OU_PEOPLE = "ou=People"; - - public static final String PERUN_USER_ID = "perunUserId"; - public static final String MEMBER_OF = "memberOf"; - - public static final String PERUN_GROUP = "perunGroup"; - public static final String PERUN_USER = "perunUser"; - public static final String PERUN_GROUP_ID = "perunGroupId"; - public static final String UNIQUE_MEMBER = "uniqueMember"; - - public static final String PERUN_VO_ID = "perunVoId"; - public static final String EDU_PERSON_PRINCIPAL_NAMES = "eduPersonPrincipalNames"; - - private final PerunConnectorLdap connectorLdap; - private final Map<String, AttributeMapping> attributeMappings; - - @Autowired - PerunAdapterLdap(PerunConnectorLdap connectorLdap, AttributesConfig attributesConfig) { - this.connectorLdap = connectorLdap; - this.attributeMappings = attributesConfig.getAttributeMappings(); - } - - @Override - public Long getPreauthenticatedUserId(String extLogin, String extSourceName) { - FilterBuilder filter = and( - equal(OBJECT_CLASS, PERUN_USER), equal(EDU_PERSON_PRINCIPAL_NAMES, extLogin) - ); - - return getPerunUserId(filter); - } - - @Override - public boolean isUserInGroup(Long userId, Long groupId) { - String uniqueMemberValue = PERUN_USER_ID + '=' + userId + ',' + OU_PEOPLE + ',' + connectorLdap.getBaseDN(); - - FilterBuilder filter = and( - equal(OBJECT_CLASS, PERUN_GROUP), - equal(PERUN_GROUP_ID, String.valueOf(groupId)), - equal(UNIQUE_MEMBER, uniqueMemberValue) - ); - - EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_GROUP_ID).getString()); - String[] attributes = new String[] { PERUN_GROUP_ID }; - List<Long> ids = connectorLdap.search(null, filter, SearchScope.SUBTREE, attributes, mapper); - - return ids.stream().filter(groupId::equals).count() == 1L; - } - - @Override - public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) { - Set<Long> userGroupIds = getGroupIdsWhereUserIsMember(userId, null); - if (userGroupIds.isEmpty()) { - return new ArrayList<>(); - } - - FilterBuilder[] groupIdFilters = new FilterBuilder[userGroupIds.size()]; - int i = 0; - - for (Long id: userGroupIds) { - groupIdFilters[i++] = equal(PERUN_GROUP_ID, String.valueOf(id)); - } - - AttributeMapping affiliationsMapping = attributeMappings.get(groupAffiliationsAttr); - FilterBuilder filterBuilder = and(equal(OBJECT_CLASS, PERUN_GROUP), or(groupIdFilters)); - String[] attributes = new String[] { affiliationsMapping.getLdapName() }; - - EntryMapper<Set<Affiliation>> mapper = e -> { - Set<Affiliation> affiliations = new HashSet<>(); - if (!checkHasAttributes(e, attributes)) { - return affiliations; - } - - Attribute a = e.get(affiliationsMapping.getLdapName()); - long linuxTime = System.currentTimeMillis() / 1000L; - a.iterator().forEachRemaining(v -> affiliations.add(new Affiliation(null, v.getString(), linuxTime))); - - return affiliations; - }; - - List<Set<Affiliation>> affiliationSets = connectorLdap.search(null, filterBuilder, SearchScope.SUBTREE, attributes, mapper); - - return affiliationSets.stream().flatMap(Set::stream).distinct().collect(Collectors.toList()); - } - - @Override - public Set<Long> getUserIdsByAttributeValue(AttributeMapping attrName, String attrValue) { - if (!StringUtils.hasText(attrName.getLdapName())) { - return new HashSet<>(); - } - - FilterBuilder filter = and( - equal(OBJECT_CLASS, PERUN_USER), - equal(attrName.getLdapName(), attrValue) - ); - - SearchScope scope = SearchScope.ONELEVEL; - String[] attributes = new String[]{PERUN_USER_ID}; - EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_USER_ID).getString()); - - List<Long> result = connectorLdap.search(OU_PEOPLE, filter, scope, attributes, mapper); - - return Set.copyOf(result); - } - - private Set<Long> getGroupIdsWhereUserIsMember(Long userId, Long voId) { - String dnPrefix = getDnPrefixForUserId(userId); - String[] attributes = new String[] { MEMBER_OF }; - - EntryMapper<Set<Long>> mapper = e -> { - Set<Long> ids = new HashSet<>(); - if (checkHasAttributes(e, attributes)) { - Attribute a = e.get(MEMBER_OF); - a.iterator().forEachRemaining(id -> { - String fullVal = id.getString(); - String[] parts = fullVal.split(",", 3); - - String groupId = parts[0]; - groupId = groupId.replace(PERUN_GROUP_ID + '=', ""); - - String voIdStr = parts[1]; - voIdStr = voIdStr.replace(PERUN_VO_ID + '=', ""); - - if (voId == null || voId.equals(Long.parseLong(voIdStr))) { - ids.add(Long.parseLong(groupId)); - } - }); - } - - return ids; - }; - - Set<Long> res = connectorLdap.lookup(dnPrefix, attributes, mapper); - if (res == null) { - res = new HashSet<>(); - } - - return res; - } - - private String getDnPrefixForUserId(Long userId) { - return PERUN_USER_ID + '=' + userId + ',' + OU_PEOPLE; - } - - private boolean checkHasAttributes(Entry e, String[] attributes) { - if (e == null) { - return false; - } else if (attributes == null) { - return true; - } - - for (String attr: attributes) { - if (e.get(attr) == null) { - return false; - } - } - - return true; - } - - private Long getPerunUserId(FilterBuilder filter) { - SearchScope scope = SearchScope.ONELEVEL; - String[] attributes = new String[]{PERUN_USER_ID}; - EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_USER_ID).getString()); - - return connectorLdap.searchFirst(OU_PEOPLE, filter, scope, attributes, mapper); - } -} diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterRpc.java b/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterRpc.java deleted file mode 100644 index 7747579..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/adapters/impl/PerunAdapterRpc.java +++ /dev/null @@ -1,283 +0,0 @@ -package cz.muni.ics.ga4gh.adapters.impl; - -import com.fasterxml.jackson.databind.JsonNode; -import cz.muni.ics.ga4gh.adapters.PerunAdapterMethods; -import cz.muni.ics.ga4gh.adapters.PerunAdapterMethodsRpc; -import cz.muni.ics.ga4gh.config.AttributesConfig; -import cz.muni.ics.ga4gh.config.RpcConfig; -import cz.muni.ics.ga4gh.connectors.PerunConnectorRpc; -import cz.muni.ics.ga4gh.mappers.RpcMapper; -import cz.muni.ics.ga4gh.model.Affiliation; -import cz.muni.ics.ga4gh.model.AttributeMapping; -import cz.muni.ics.ga4gh.model.Group; -import cz.muni.ics.ga4gh.model.Member; -import cz.muni.ics.ga4gh.model.PerunAttributeValue; -import cz.muni.ics.ga4gh.model.UserExtSource; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import static cz.muni.ics.ga4gh.enums.MemberStatus.VALID; - -@Component -@Slf4j -public class PerunAdapterRpc implements PerunAdapterMethods, PerunAdapterMethodsRpc { - - public static final String EXT_LOGIN = "extLogin"; - public static final String EXT_SOURCE_NAME = "extSourceName"; - public static final String USER_EXT_SOURCE = "userExtSource"; - public static final String ATTR_NAMES = "attrNames"; - public static final String VALUE_CREATED_AT = "valueCreatedAt"; - - public static final String ID = "id"; - public static final String VO = "vo"; - public static final String USER = "user"; - public static final String MEMBER = "member"; - public static final String GROUP = "group"; - - public static final String ATTRIBUTE_NAME = "attributeName"; - public static final String ATTRIBUTE_VALUE = "attributeValue"; - - public static final String ATTRIBUTES_MANAGER = "attributesManager"; - public static final String GROUPS_MANAGER = "groupsManager"; - public static final String MEMBERS_MANAGER = "membersManager"; - public static final String USERS_MANAGER = "usersManager"; - - public static final String GET_USER_BY_EXT_SOURCE_NAME_AND_EXT_LOGIN = "getUserByExtSourceNameAndExtLogin"; - public static final String GET_USER_EXT_SOURCES = "getUserExtSources"; - public static final String GET_GROUP_BY_ID = "getGroupById"; - public static final String GET_MEMBER_BY_USER = "getMemberByUser"; - public static final String GET_MEMBERS_BY_USER = "getMembersByUser"; - public static final String GET_MEMBER_GROUPS = "getMemberGroups"; - public static final String IS_GROUP_MEMBER = "isGroupMember"; - public static final String GET_ATTRIBUTE = "getAttribute"; - public static final String GET_ATTRIBUTES = "getAttributes"; - public static final String GET_USERS_BY_ATTRIBUTE_VALUE = "getUsersByAttributeValue"; - - private final PerunConnectorRpc connectorRpc; - private final Map<String, AttributeMapping> attributeMappings; - - @Autowired - PerunAdapterRpc(PerunConnectorRpc connectorRpc, AttributesConfig attributesConfig) { - this.connectorRpc = connectorRpc; - this.attributeMappings = attributesConfig.getAttributeMappings(); - } - - @Override - public Long getPreauthenticatedUserId(String extLogin, String extSourceName) { - if (!connectorRpc.isEnabled()) { - return null; - } - - Map<String, Object> map = new LinkedHashMap<>(); - map.put(EXT_LOGIN, extLogin); - map.put(EXT_SOURCE_NAME, extSourceName); - - JsonNode response = connectorRpc.post(USERS_MANAGER, GET_USER_BY_EXT_SOURCE_NAME_AND_EXT_LOGIN, map); - - return response.get(ID) == null ? null : response.get(ID).asLong(); - } - - @Override - public boolean isUserInGroup(Long userId, Long groupId) { - if (!connectorRpc.isEnabled()) { - return false; - } - - Map<String, Object> groupParams = new LinkedHashMap<>(); - groupParams.put(ID, groupId); - JsonNode groupResponse = connectorRpc.post(GROUPS_MANAGER, GET_GROUP_BY_ID, groupParams); - Group group = RpcMapper.mapGroup(groupResponse); - - Map<String, Object> memberParams = new LinkedHashMap<>(); - memberParams.put(VO, group.getVoId()); - memberParams.put(USER, userId); - JsonNode memberResponse = connectorRpc.post(MEMBERS_MANAGER, GET_MEMBER_BY_USER, memberParams); - Member member = RpcMapper.mapMember(memberResponse); - - Map<String, Object> isGroupMemberParams = new LinkedHashMap<>(); - isGroupMemberParams.put(GROUP, groupId); - isGroupMemberParams.put(MEMBER, member.getId()); - JsonNode res = connectorRpc.post(GROUPS_MANAGER, IS_GROUP_MEMBER, isGroupMemberParams); - - return res.asBoolean(false); - } - - @Override - public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) { - if (!connectorRpc.isEnabled()) { - return new ArrayList<>(); - } - - List<Affiliation> affiliations = new ArrayList<>(); - List<Member> userMembers = getMembersByUser(userId); - - for (Member member : userMembers) { - if (VALID.equals(member.getStatus())) { - List<Group> memberGroups = getMemberGroups(member.getId()); - for (Group group : memberGroups) { - PerunAttributeValue attrValue = this.getGroupAttributeValue(group, groupAffiliationsAttr); - if (attrValue != null && attrValue.valueAsList() != null) { - long linuxTime = System.currentTimeMillis() / 1000L; - - for (String value : attrValue.valueAsList()) { - Affiliation affiliation = new Affiliation(null, value, linuxTime); - log.debug("found {} on group {}", value, group.getName()); - affiliations.add(affiliation); - } - } - } - } - } - - return affiliations; - } - - @Override - public String getUserAttributeCreatedAt(Long userId, String attrName) { - if (!connectorRpc.isEnabled() || attributeMappings.get(attrName) == null) { - return null; - } - - Map<String, Object> map = new LinkedHashMap<>(); - map.put(USER, userId); - map.put(ATTRIBUTE_NAME, attributeMappings.get(attrName).getRpcName()); - - JsonNode res = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTE, map); - - if (res == null || !res.hasNonNull(VALUE_CREATED_AT)) { - return null; - } - - return res.get(VALUE_CREATED_AT).asText(); - } - - @Override - public List<Affiliation> getUserExtSourcesAffiliations(Long userId, String affiliationsAttr, String orgUrlAttr) { - if (!connectorRpc.isEnabled()) { - return new ArrayList<>(); - } - - List<UserExtSource> userExtSources = getUserExtSources(userId); - List<Affiliation> affiliations = new ArrayList<>(); - - Map<String, AttributeMapping> attrMappings = new HashMap<>(); - attrMappings.put(affiliationsAttr, attributeMappings.get(affiliationsAttr)); - attrMappings.put(orgUrlAttr, attributeMappings.get(orgUrlAttr)); - - for (UserExtSource ues : userExtSources) { - if ("cz.metacentrum.perun.core.impl.ExtSourceIdp".equals(ues.getExtSource().getType())) { - Map<String, PerunAttributeValue> uesAttrValues = getUserExtSourceAttributeValues(ues.getId(), attrMappings); - - long asserted = ues.getLastAccess().getTime() / 1000L; - - String orgUrl = uesAttrValues.get(affiliationsAttr).valueAsString(); - String affs = uesAttrValues.get(orgUrlAttr).valueAsString(); - - if (affs != null) { - for (String aff : affs.split(";")) { - String source = ( (orgUrl != null) ? orgUrl : ues.getExtSource().getName() ); - Affiliation affiliation = new Affiliation(source, aff, asserted); - log.debug("found {} from IdP {} with orgURL {} asserted at {}", aff, ues.getExtSource().getName(), - orgUrl, asserted); - affiliations.add(affiliation); - } - } - } - } - - return affiliations; - } - - @Override - public Set<Long> getUserIdsByAttributeValue(AttributeMapping attrName, String attrValue) { - if (!connectorRpc.isEnabled()) { - return new HashSet<>(); - } - - Set<Long> result = new HashSet<>(); - - if (!StringUtils.hasText(attrName.getRpcName())) { - return result; - } - - Map<String, Object> map = new LinkedHashMap<>(); - map.put(ATTRIBUTE_NAME, attrName.getRpcName()); - map.put(ATTRIBUTE_VALUE, attrValue); - - JsonNode res = connectorRpc.post(USERS_MANAGER, GET_USERS_BY_ATTRIBUTE_VALUE, map); - - if (res != null) { - for (int i = 0; i < res.size(); i++) { - result.add(res.get(i).get(ID).asLong()); - } - } - - return result; - } - - private List<Member> getMembersByUser(Long userId) { - if (!this.connectorRpc.isEnabled()) { - return new ArrayList<>(); - } - - Map<String, Object> params = new LinkedHashMap<>(); - params.put(USER, userId); - JsonNode jsonNode = connectorRpc.post(MEMBERS_MANAGER, GET_MEMBERS_BY_USER, params); - - return RpcMapper.mapMembers(jsonNode); - } - - private List<Group> getMemberGroups(Long memberId) { - if (!this.connectorRpc.isEnabled()) { - return new ArrayList<>(); - } - - Map<String, Object> map = new LinkedHashMap<>(); - map.put(MEMBER, memberId); - - JsonNode response = connectorRpc.post(GROUPS_MANAGER, GET_MEMBER_GROUPS, map); - return RpcMapper.mapGroups(response); - } - - private PerunAttributeValue getGroupAttributeValue(Group group, String attrToFetch) { - if (attributeMappings.get(attrToFetch) == null) { - return null; - } - - Map<String, Object> map = new LinkedHashMap<>(); - map.put(GROUP, group.getId()); - map.put(ATTRIBUTE_NAME, attributeMappings.get(attrToFetch).getRpcName()); - JsonNode res = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTE, map); - - return RpcMapper.mapAttributeValue(res); - } - - private List<UserExtSource> getUserExtSources(Long userId) { - Map<String, Object> map = new LinkedHashMap<>(); - map.put(USER, userId); - - JsonNode response = connectorRpc.post(USERS_MANAGER, GET_USER_EXT_SOURCES, map); - return RpcMapper.mapUserExtSources(response); - } - - private Map<String, PerunAttributeValue> getUserExtSourceAttributeValues(Long uesId, Map<String, AttributeMapping> attrMappings) { - Map<String, Object> map = new LinkedHashMap<>(); - map.put(USER_EXT_SOURCE, uesId); - map.put(ATTR_NAMES, attrMappings.values().stream().map(AttributeMapping::getRpcName).collect(Collectors.toList())); - - JsonNode response = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTES, map); - - return RpcMapper.mapAttributes(response, attrMappings); - } -} diff --git a/src/main/java/cz/muni/ics/ga4gh/aop/LogTimes.java b/src/main/java/cz/muni/ics/ga4gh/aop/LogTimes.java deleted file mode 100644 index 0e2e60b..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/aop/LogTimes.java +++ /dev/null @@ -1,11 +0,0 @@ -package cz.muni.ics.ga4gh.aop; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.METHOD) -@Retention(RetentionPolicy.RUNTIME) -public @interface LogTimes { -} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/AdapterBeans.java b/src/main/java/cz/muni/ics/ga4gh/base/AdapterBeans.java new file mode 100644 index 0000000..631d00f --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/AdapterBeans.java @@ -0,0 +1,68 @@ +package cz.muni.ics.ga4gh.base; + +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import cz.muni.ics.ga4gh.base.adapters.impl.PerunAdapterImpl; +import cz.muni.ics.ga4gh.base.adapters.impl.PerunAdapterLdap; +import cz.muni.ics.ga4gh.base.adapters.impl.PerunAdapterRpc; +import cz.muni.ics.ga4gh.base.connectors.PerunConnectorLdap; +import cz.muni.ics.ga4gh.base.connectors.PerunConnectorRpc; +import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException; +import cz.muni.ics.ga4gh.base.properties.AttributeMappingProperties; +import cz.muni.ics.ga4gh.base.properties.PerunAdapterProperties; +import cz.muni.ics.ga4gh.base.properties.PerunLdapConnectorProperties; +import cz.muni.ics.ga4gh.base.properties.PerunRpcConnectorProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +@Component +public class AdapterBeans { + + @Bean + public PerunAdapterRpc perunAdapterRpc(PerunConnectorRpc connectorRpc, + AttributeMappingProperties attributeMappingProperties) + { + return new PerunAdapterRpc(connectorRpc, attributeMappingProperties); + } + + @Bean + public PerunConnectorRpc perunConnectorRpc(PerunRpcConnectorProperties rpcConnectorProperties) { + return new PerunConnectorRpc(rpcConnectorProperties); + } + + @Bean + @ConditionalOnBean(PerunLdapConnectorProperties.class) + public PerunConnectorLdap perunConnectorLdap(PerunLdapConnectorProperties ldapConnectorProperties) + { + return new PerunConnectorLdap(ldapConnectorProperties); + } + + @Bean + @ConditionalOnBean(PerunConnectorLdap.class) + public PerunAdapterLdap perunAdapterLdap(PerunConnectorLdap connectorLdap, + AttributeMappingProperties attributeMappingProperties) + { + return new PerunAdapterLdap(connectorLdap, attributeMappingProperties); + } + + @Bean + @ConditionalOnBean(PerunAdapterLdap.class) + public PerunAdapter perunAdapterLdapRpc(PerunAdapterProperties adapterProperties, + PerunAdapterRpc adapterRpc, + PerunAdapterLdap adapterLdap) + throws ConfigurationException + { + return new PerunAdapterImpl(adapterProperties, adapterRpc, adapterLdap); + } + + @Bean + @ConditionalOnMissingBean(PerunAdapterLdap.class) + public PerunAdapter perunAdapterRpcOnly(PerunAdapterProperties adapterProperties, + PerunAdapterRpc adapterRpc) + throws ConfigurationException + { + return new PerunAdapterImpl(adapterProperties, adapterRpc); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/jose/keystore/JWKSetKeyStore.java b/src/main/java/cz/muni/ics/ga4gh/base/JWKSetKeyStore.java similarity index 98% rename from src/main/java/cz/muni/ics/ga4gh/jose/keystore/JWKSetKeyStore.java rename to src/main/java/cz/muni/ics/ga4gh/base/JWKSetKeyStore.java index 0c03f88..5655804 100644 --- a/src/main/java/cz/muni/ics/ga4gh/jose/keystore/JWKSetKeyStore.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/JWKSetKeyStore.java @@ -1,12 +1,7 @@ -package cz.muni.ics.ga4gh.jose.keystore; +package cz.muni.ics.ga4gh.base; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; -import lombok.Getter; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.Resource; -import org.springframework.stereotype.Component; - import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -14,6 +9,10 @@ import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.List; import java.util.stream.Collectors; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Component; @Component @Getter diff --git a/src/main/java/cz/muni/ics/ga4gh/base/Utils.java b/src/main/java/cz/muni/ics/ga4gh/base/Utils.java new file mode 100644 index 0000000..535c86c --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/Utils.java @@ -0,0 +1,332 @@ +package cz.muni.ics.ga4gh.base; + +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.ASSERTED; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.CONDITIONS; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.SOURCE; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.VALUE; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKMatcher; +import com.nimbusds.jose.jwk.JWKSelector; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.RemoteJWKSet; +import com.nimbusds.jose.proc.SecurityContext; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.JWTParser; +import com.nimbusds.jwt.SignedJWT; +import cz.muni.ics.ga4gh.base.model.ClaimRepositoryHeader; +import cz.muni.ics.ga4gh.base.model.Ga4ghClaimRepository; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisaV1; +import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties; +import cz.muni.ics.ga4gh.base.properties.Ga4ghClaimRepositoryProperties; +import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService; +import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.InterceptingClientHttpRequestFactory; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +@Slf4j +public class Utils { + + public static void initializeClaimRepositories(BrokerInstanceProperties brokerProperties, + List<Ga4ghClaimRepository> claimRepositories, + Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets, + Map<URI, String> signers) + { + for (Ga4ghClaimRepositoryProperties ga4ghClaimRepositoryProperties : brokerProperties.getPassportRepositories()) { + initializeClaimRepository(ga4ghClaimRepositoryProperties, claimRepositories); + initializeSigner(signers, remoteJwkSets, ga4ghClaimRepositoryProperties.getName(), ga4ghClaimRepositoryProperties.getJwks()); + } + } + + public static Ga4ghPassportVisa createVisa(VisaAssemblyParameters parameters) + { + long now = System.currentTimeMillis() / 1000L; + + if (parameters.getAsserted() > now) { + log.warn("Visa asserted in future, it will be ignored!"); + log.debug("Visa information: {}", parameters); + return null; + } + + if (parameters.getExpires() <= now) { + log.warn("Visa is expired, it will be ignored!"); + log.debug("Visa information: {}", parameters); + return null; + } + + Ga4ghPassportVisaV1 ga4ghVisaV1 = new Ga4ghPassportVisaV1(); + ga4ghVisaV1.setAsserted(parameters.getAsserted()); + ga4ghVisaV1.setBy(parameters.getBy()); + ga4ghVisaV1.setValue(parameters.getValue()); + ga4ghVisaV1.setType(parameters.getType()); + ga4ghVisaV1.setSource(parameters.getSource()); + ga4ghVisaV1.setConditions(parameters.getConditions()); + + Ga4ghPassportVisa visa = new Ga4ghPassportVisa(); + visa.setKid(parameters.getJwtService().getSignerKeyId()); + visa.setTyp(JOSEObjectType.JWT); + visa.setJku(parameters.getJku()); + + visa.setIss(parameters.getIssuer()); + visa.setIat(new Date()); + visa.setExp(new Date(parameters.getExpires() * 1000L)); + visa.setSub(parameters.getSub()); + visa.setJti(UUID.randomUUID().toString()); + visa.setGa4ghVisaV1(ga4ghVisaV1); + + visa.setVerified(true); + visa.setSigner(parameters.getSigner()); + visa.generateSignedJwt(parameters.getJwtService()); + + return visa; + } + + public static Ga4ghPassportVisa parseVisa(String jwtString) { + try { + SignedJWT signedJWT = (SignedJWT) JWTParser.parse(jwtString); + return parseVisa(signedJWT); + } catch (Exception ex) { + log.error("Visa '{}' cannot be parsed", jwtString, ex); + } + return null; + } + + public static Ga4ghPassportVisa parseVisa(SignedJWT jwt) + throws ParseException, JsonProcessingException + { + JWSHeader header = jwt.getHeader(); + JWTClaimsSet payloadClaimSet = jwt.getJWTClaimsSet(); + + Ga4ghPassportVisa visa = new Ga4ghPassportVisa(); + + JsonNode visaPayload = new ObjectMapper().readValue(jwt.getPayload().toString(), JsonNode.class); + JsonNode visaV1 = visaPayload.path(Ga4ghPassportVisaV1.GA4GH_VISA_V1); + visa.setVerified(checkVisaHeader(header) && checkVisaValue(visaV1)); + + if (!visa.isVerified()) { + log.debug("Visa '{}' (payload '{}') did not pass verification", jwt, visaPayload); + return visa; + } + + visa.setKid(header.getKeyID()); + visa.setJku(header.getJWKURL()); + visa.setTyp(header.getType()); + + visa.setSub(payloadClaimSet.getSubject()); + visa.setIss(payloadClaimSet.getIssuer()); + visa.setIat(payloadClaimSet.getIssueTime()); + visa.setExp(payloadClaimSet.getExpirationTime()); + visa.setJti(payloadClaimSet.getJWTID()); + visa.setGa4ghVisaV1(parseVisaV1(visaV1)); + visa.setLinkedIdentity(Utils.constructLinkedIdentity(visa.getSub(), visa.getIss())); + visa.setJwt(jwt); + + return visa; + } + + private static boolean checkVisaExpiration(Ga4ghPassportVisa visa) { + long exp = visa.getExp().getTime(); + boolean expirationValid = exp > Instant.now().getEpochSecond(); + if (!expirationValid) { + log.warn("Visa did not pass expiration validation. Visa expired on '{}'.", isoDateTime(exp)); + } + return expirationValid; + } + + public static Ga4ghPassportVisaV1 parseVisaV1(JsonNode visaV1Json) { + Ga4ghPassportVisaV1 visaV1 = new Ga4ghPassportVisaV1(); + visaV1.setAsserted(visaV1Json.path(ASSERTED).longValue()); + visaV1.setSource(visaV1Json.path(SOURCE).textValue()); + visaV1.setType(visaV1Json.path(TYPE).textValue()); + visaV1.setValue(visaV1Json.path(VALUE).textValue()); + if (visaV1Json.hasNonNull(BY)) { + visaV1.setBy(visaV1Json.path(BY).textValue()); + } + if (visaV1Json.hasNonNull(CONDITIONS)) { + visaV1.setConditions(visaV1Json.get(CONDITIONS)); + } + return visaV1; + } + + private static boolean checkVisaHeader(JWSHeader visaHeader) { + boolean valid = visaHeader.getJWKURL() != null + && visaHeader.getType() != null + && StringUtils.hasText(visaHeader.getKeyID()); + if (!valid) { + log.debug("Visa header did not pass verification"); + } + return valid; + } + + private static boolean checkVisaValue(JsonNode visaV1) { + if (visaV1.isMissingNode() || visaV1.isNull() || visaV1.isEmpty()) { + log.warn("Visa value ({}) is not present. Visa did not pass value verification.", + Ga4ghPassportVisaV1.GA4GH_VISA_V1); + return false; + } + boolean keysValid = checkKeyPresence(visaV1, TYPE) + && checkKeyPresence(visaV1, Ga4ghPassportVisa.ASSERTED) + && checkKeyPresence(visaV1, Ga4ghPassportVisa.VALUE) + && checkKeyPresence(visaV1, Ga4ghPassportVisa.SOURCE); + if (!keysValid) { + log.debug("Visa value did not pass verification of required keys presence."); + } + return keysValid; + } + + private static boolean checkKeyPresence(JsonNode jsonNode, String key) { + if (jsonNode.path(key).isMissingNode()) { + log.warn("Key '{}' is missing in the Visa.", key); + return false; + } + return true; + } + + public static void verifyVisa(Ga4ghPassportVisa visa, + Map<URI, String> signers, + Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets) + { + SignedJWT jwt = visa.getJwt(); + try { + boolean expirationVerified = checkVisaExpiration(visa); + if (!expirationVerified) { + log.warn("Visa is expired, Visa verification failed"); + visa.setVerified(false); + return; + } + + URI jku = visa.getJku(); + if (jku == null) { + log.warn("JKU is missing in Visa, verification did not pass "); + visa.setVerified(false); + return; + } + + String signer = signers.getOrDefault(jku, null); + if (signer == null) { + log.warn("No signer found for JKU '{}, Visa verification failed'", jku); + visa.setVerified(false); + return; + } else { + visa.setSigner(signer); + } + + RemoteJWKSet<SecurityContext> remoteJWKSet = remoteJwkSets.getOrDefault(jku, null); + if (remoteJWKSet == null) { + log.error("Trusted key sets does not contain JKU '{}', Visa verification failed", jku); + visa.setVerified(false); + return; + } + + List<JWK> keys = remoteJWKSet.get(new JWKSelector( + new JWKMatcher.Builder().keyID(jwt.getHeader().getKeyID()).build()), null); + RSASSAVerifier verifier = new RSASSAVerifier(((RSAKey) keys.get(0)).toRSAPublicKey()); + boolean signatureVerified = jwt.verify(verifier); + if (!signatureVerified) { + log.warn("Visa signature verification failed, Visa verification failed"); + visa.setVerified(false); + return; + } + visa.setVerified(true); + } catch (Exception ex) { + log.error("Visa '{}' cannot be verified", jwt, ex); + } + } + + public static long getOneYearExpires(long asserted) { + return getExpires(asserted, 1L); + } + + public static long getExpires(long asserted, long addYears) { + return Instant.ofEpochSecond(asserted).atZone(ZoneId.systemDefault()).plusYears(addYears).toEpochSecond(); + } + + private static void initializeSigner(Map<URI, String> signers, + Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets, + String name, + String jwks) + { + try { + URL jku = new URL(jwks); + remoteJwkSets.put(jku.toURI(), new RemoteJWKSet<>(jku)); + signers.put(jku.toURI(), name); + + log.info("JWKS Signer '{}' added with keys '{}'", name, jwks); + } catch (MalformedURLException | URISyntaxException e) { + log.error("cannot add to RemoteJWKSet map: '{}' -> '{}'", name, jwks, e); + } + } + + private static void initializeClaimRepository( + Ga4ghClaimRepositoryProperties ga4ghClaimRepositoryProperties, List<Ga4ghClaimRepository> claimRepositories) { + String name = ga4ghClaimRepositoryProperties.getName(); + String actionURL = ga4ghClaimRepositoryProperties.getUrl(); + List<ClaimRepositoryHeader> headers = ga4ghClaimRepositoryProperties.getHeaders(); + + if (actionURL == null || headers.isEmpty()) { + log.error("claim repository '{}' not defined with url|auth_header|auth_value", + ga4ghClaimRepositoryProperties); + return; + } + + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setRequestFactory( + new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), getClientHttpRequestInterceptors(headers)) + ); + + claimRepositories.add(new Ga4ghClaimRepository(name, actionURL, restTemplate)); + log.info("GA4GH Claims Repository '{}' configured at '{}'", name, actionURL); + } + + private static String isoDate(long linuxTime) { + return isoFormat(linuxTime, DateTimeFormatter.ISO_LOCAL_DATE); + } + + private static String isoDateTime(long linuxTime) { + return isoFormat(linuxTime, DateTimeFormatter.ISO_DATE_TIME); + } + + private static String isoFormat(long linuxTime, DateTimeFormatter formatter) { + ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(linuxTime), ZoneId.systemDefault()); + return formatter.format(zdt); + } + + private static List<ClientHttpRequestInterceptor> getClientHttpRequestInterceptors(List<ClaimRepositoryHeader> headers) { + return new ArrayList<>(headers); + } + + public static String constructLinkedIdentity(String sub, String iss) { + return URLEncoder.encode(sub, StandardCharsets.UTF_8) + + ',' + + URLEncoder.encode(iss, StandardCharsets.UTF_8); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapter.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapter.java new file mode 100644 index 0000000..f270e8c --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapter.java @@ -0,0 +1,52 @@ +package cz.muni.ics.ga4gh.base.adapters; + +import cz.muni.ics.ga4gh.base.adapters.impl.PerunAdapterLdap; +import cz.muni.ics.ga4gh.base.adapters.impl.PerunAdapterRpc; +import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException; +import cz.muni.ics.ga4gh.base.properties.PerunAdapterProperties; +import java.util.ArrayList; +import java.util.List; +import lombok.Getter; + +@Getter +public abstract class PerunAdapter implements PerunAdapterMethods { + + public static final String RPC = "RPC"; + public static final String LDAP = "LDAP"; + + private final List<PerunAdapterMethods> adaptersChain = new ArrayList<>(); + private final PerunAdapterMethodsRpc adapterRpc; + private final PerunAdapterMethodsLdap adapterLdap; + private final boolean callFallback; + + public PerunAdapter(PerunAdapterProperties config, + PerunAdapterRpc adapterRpc, + PerunAdapterLdap adapterLdap) + throws ConfigurationException + { + if (adapterRpc == null) { + throw new ConfigurationException("No Perun RPC adapter configured"); + } + + if (RPC.equalsIgnoreCase(config.getAdapterPrimary())) { + this.adaptersChain.add(adapterRpc); + if (adapterLdap != null) { + this.adaptersChain.add(adapterLdap); + } + this.adaptersChain.add(adapterRpc); + this.adaptersChain.add(adapterLdap); + } else if (LDAP.equalsIgnoreCase(config.getAdapterPrimary())) { + if (adapterLdap == null) { + throw new ConfigurationException("LDAP adapter specified as primary, but not defined"); + } + this.adaptersChain.add(adapterLdap); + } else { + throw new ConfigurationException("Unrecognized primary adapter set"); + } + + this.adapterRpc = adapterRpc; + this.adapterLdap = adapterLdap; + this.callFallback = config.isCallFallback(); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethods.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethods.java new file mode 100644 index 0000000..042727b --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethods.java @@ -0,0 +1,28 @@ +package cz.muni.ics.ga4gh.base.adapters; + +import cz.muni.ics.ga4gh.base.model.Affiliation; +import java.util.List; +import java.util.Set; + +public interface PerunAdapterMethods { + + /** + * Fetch user based on his principal (extLogin and extSource) from Perun + * + * @return PerunUser with id of found user + */ + Long getPerunUserId(String extLogin, String extSourceName); + + boolean isUserInGroup(Long userId, Long groupId); + + List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr); + + List<Affiliation> getGroupAffiliations(Long userId, Long voId, String groupAffiliationsAttr); + + Set<Long> getUserIdsByAttributeValue(String attrName, String attrValue); + + String getUserSub(Long userId, String subAttribute); + + boolean isUserInVo(Long userId, Long voId); + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsLdap.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsLdap.java similarity index 52% rename from src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsLdap.java rename to src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsLdap.java index 70c67cd..d589f81 100644 --- a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsLdap.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsLdap.java @@ -1,4 +1,4 @@ -package cz.muni.ics.ga4gh.adapters; +package cz.muni.ics.ga4gh.base.adapters; public interface PerunAdapterMethodsLdap { } diff --git a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsRpc.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsRpc.java similarity index 54% rename from src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsRpc.java rename to src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsRpc.java index f2c4d5e..195a5d6 100644 --- a/src/main/java/cz/muni/ics/ga4gh/adapters/PerunAdapterMethodsRpc.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunAdapterMethodsRpc.java @@ -1,7 +1,7 @@ -package cz.muni.ics.ga4gh.adapters; - -import cz.muni.ics.ga4gh.model.Affiliation; +package cz.muni.ics.ga4gh.base.adapters; +import cz.muni.ics.ga4gh.base.model.Affiliation; +import cz.muni.ics.ga4gh.base.model.UserExtSource; import java.util.List; public interface PerunAdapterMethodsRpc { @@ -9,4 +9,7 @@ public interface PerunAdapterMethodsRpc { String getUserAttributeCreatedAt(Long userId, String attrName); List<Affiliation> getUserExtSourcesAffiliations(Long userId, String affiliationsAttr, String orgUrlAttr); + + List<UserExtSource> getIdpUserExtSources(Long perunUserId); + } diff --git a/src/main/java/cz/muni/ics/ga4gh/mappers/RpcMapper.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunRpcAdapterMapper.java similarity index 91% rename from src/main/java/cz/muni/ics/ga4gh/mappers/RpcMapper.java rename to src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunRpcAdapterMapper.java index 37254d6..f09da28 100644 --- a/src/main/java/cz/muni/ics/ga4gh/mappers/RpcMapper.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/PerunRpcAdapterMapper.java @@ -1,28 +1,27 @@ -package cz.muni.ics.ga4gh.mappers; +package cz.muni.ics.ga4gh.base.adapters; import com.fasterxml.jackson.databind.JsonNode; +import cz.muni.ics.ga4gh.base.enums.MemberStatus; +import cz.muni.ics.ga4gh.base.exceptions.MissingFieldException; +import cz.muni.ics.ga4gh.base.model.AttributeMapping; +import cz.muni.ics.ga4gh.base.model.ExtSource; +import cz.muni.ics.ga4gh.base.model.Group; +import cz.muni.ics.ga4gh.base.model.Member; +import cz.muni.ics.ga4gh.base.model.PerunAttributeValue; +import cz.muni.ics.ga4gh.base.model.UserExtSource; import java.sql.Timestamp; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import cz.muni.ics.ga4gh.enums.MemberStatus; -import cz.muni.ics.ga4gh.exceptions.MissingFieldException; -import cz.muni.ics.ga4gh.model.AttributeMapping; -import cz.muni.ics.ga4gh.model.ExtSource; -import cz.muni.ics.ga4gh.model.Group; -import cz.muni.ics.ga4gh.model.Member; -import cz.muni.ics.ga4gh.model.PerunAttributeValue; -import cz.muni.ics.ga4gh.model.UserExtSource; - /** * This class is mapping JsonNodes to object models. * * @author Dominik Frantisek Bucik <bucik@ics.muni.cz> */ -public class RpcMapper { +public class PerunRpcAdapterMapper { public static final String ID = "id"; public static final String UUID = "uuid"; @@ -77,7 +76,7 @@ public class RpcMapper { List<Group> result = new ArrayList<>(); for (int i = 0; i < jsonArray.size(); i++) { JsonNode groupNode = jsonArray.get(i); - Group mappedGroup = RpcMapper.mapGroup(groupNode); + Group mappedGroup = PerunRpcAdapterMapper.mapGroup(groupNode); result.add(mappedGroup); } @@ -117,7 +116,7 @@ public class RpcMapper { List<Member> members = new ArrayList<>(); for (int i = 0; i < jsonArray.size(); i++) { JsonNode memberNode = jsonArray.get(i); - Member mappedMember = RpcMapper.mapMember(memberNode); + Member mappedMember = PerunRpcAdapterMapper.mapMember(memberNode); members.add(mappedMember); } @@ -155,7 +154,7 @@ public class RpcMapper { Long id = getRequiredFieldAsLong(json, ID); String login = getRequiredFieldAsString(json, LOGIN); - ExtSource extSource = RpcMapper.mapExtSource(getRequiredFieldAsJsonNode(json, EXT_SOURCE)); + ExtSource extSource = PerunRpcAdapterMapper.mapExtSource(getRequiredFieldAsJsonNode(json, EXT_SOURCE)); int loa = getRequiredFieldAsInt(json, LOA); boolean persistent = getRequiredFieldAsBoolean(json, PERSISTENT); Timestamp lastAccess = Timestamp.valueOf(getRequiredFieldAsString(json, LAST_ACCESS)); @@ -178,7 +177,7 @@ public class RpcMapper { for (int i = 0; i < jsonArray.size(); i++) { JsonNode userExtSource = jsonArray.get(i); - UserExtSource mappedUes = RpcMapper.mapUserExtSource(userExtSource); + UserExtSource mappedUes = PerunRpcAdapterMapper.mapUserExtSource(userExtSource); userExtSources.add(mappedUes); } diff --git a/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterImpl.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterImpl.java new file mode 100644 index 0000000..e018c08 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterImpl.java @@ -0,0 +1,128 @@ +package cz.muni.ics.ga4gh.base.adapters.impl; + +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import cz.muni.ics.ga4gh.base.adapters.PerunAdapterMethods; +import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException; +import cz.muni.ics.ga4gh.base.exceptions.PerunAdapterOperationException; +import cz.muni.ics.ga4gh.base.model.Affiliation; +import cz.muni.ics.ga4gh.base.properties.PerunAdapterProperties; +import java.util.List; +import java.util.Set; + +public class PerunAdapterImpl extends PerunAdapter { + + + public PerunAdapterImpl(PerunAdapterProperties config, + PerunAdapterRpc adapterRpc, + PerunAdapterLdap adapterLdap) + throws ConfigurationException + { + super(config, adapterRpc, adapterLdap); + } + + public PerunAdapterImpl(PerunAdapterProperties config, + PerunAdapterRpc adapterRpc) + throws ConfigurationException + { + super(config, adapterRpc, null); + } + + @Override + public Long getPerunUserId(String extLogin, String extSourceName) { + try { + for (PerunAdapterMethods adapter: getAdaptersChain()) { + return adapter.getPerunUserId(extLogin, extSourceName); + } + } catch (UnsupportedOperationException e) { + if (!this.isCallFallback()) { + throw e; + } + } + throw new PerunAdapterOperationException("No adapter able to perform call"); + } + + @Override + public boolean isUserInGroup(Long userId, Long groupId) { + try { + for (PerunAdapterMethods adapter: getAdaptersChain()) { + return adapter.isUserInGroup(userId, groupId); + } + } catch (UnsupportedOperationException e) { + if (!this.isCallFallback()) { + throw e; + } + } + throw new PerunAdapterOperationException("No adapter able to perform call"); + } + + @Override + public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) { + try { + for (PerunAdapterMethods adapter: getAdaptersChain()) { + return adapter.getGroupAffiliations(userId, groupAffiliationsAttr); + } + } catch (UnsupportedOperationException e) { + if (!this.isCallFallback()) { + throw e; + } + } + throw new PerunAdapterOperationException("No adapter able to perform call"); + } + + @Override + public List<Affiliation> getGroupAffiliations(Long userId, Long voId, String groupAffiliationsAttr) { + try { + for (PerunAdapterMethods adapter: getAdaptersChain()) { + return adapter.getGroupAffiliations(userId, voId, groupAffiliationsAttr); + } + } catch (UnsupportedOperationException e) { + if (!this.isCallFallback()) { + throw e; + } + } + throw new PerunAdapterOperationException("No adapter able to perform call"); + } + + @Override + public Set<Long> getUserIdsByAttributeValue(String attrName, String attrValue) { + try { + for (PerunAdapterMethods adapter: getAdaptersChain()) { + return adapter.getUserIdsByAttributeValue(attrName, attrValue); + } + } catch (UnsupportedOperationException e) { + if (!this.isCallFallback()) { + throw e; + } + } + throw new PerunAdapterOperationException("No adapter able to perform call"); + } + + @Override + public String getUserSub(Long userId, String subAttribute) { + try { + for (PerunAdapterMethods adapter: getAdaptersChain()) { + return adapter.getUserSub(userId, subAttribute); + } + } catch (UnsupportedOperationException e) { + if (!this.isCallFallback()) { + throw e; + } + } + throw new PerunAdapterOperationException("No adapter able to perform call"); + } + + @Override + public boolean isUserInVo(Long userId, Long voId) { + try { + for (PerunAdapterMethods adapter: getAdaptersChain()) { + return adapter.isUserInVo(userId, voId); + } + } catch (UnsupportedOperationException e) { + if (!this.isCallFallback()) { + throw e; + } + } + throw new PerunAdapterOperationException("No adapter able to perform call"); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterLdap.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterLdap.java new file mode 100644 index 0000000..a896427 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterLdap.java @@ -0,0 +1,350 @@ +package cz.muni.ics.ga4gh.base.adapters.impl; + +import static org.apache.directory.ldap.client.api.search.FilterBuilder.and; +import static org.apache.directory.ldap.client.api.search.FilterBuilder.equal; +import static org.apache.directory.ldap.client.api.search.FilterBuilder.or; + +import cz.muni.ics.ga4gh.base.adapters.PerunAdapterMethods; +import cz.muni.ics.ga4gh.base.adapters.PerunAdapterMethodsLdap; +import cz.muni.ics.ga4gh.base.connectors.PerunConnectorLdap; +import cz.muni.ics.ga4gh.base.model.Affiliation; +import cz.muni.ics.ga4gh.base.model.AttributeMapping; +import cz.muni.ics.ga4gh.base.properties.AttributeMappingProperties; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.apache.directory.api.ldap.model.entry.Attribute; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.message.SearchScope; +import org.apache.directory.ldap.client.api.search.FilterBuilder; +import org.apache.directory.ldap.client.template.EntryMapper; +import org.springframework.util.StringUtils; + +@Slf4j +public class PerunAdapterLdap implements PerunAdapterMethods, PerunAdapterMethodsLdap { + + public static final String OBJECT_CLASS = "objectClass"; + public static final String OU_PEOPLE = "ou=People"; + + public static final String PERUN_USER_ID = "perunUserId"; + public static final String MEMBER_OF = "memberOf"; + + public static final String PERUN_GROUP = "perunGroup"; + public static final String PERUN_USER = "perunUser"; + public static final String PERUN_VO = "perunVo"; + public static final String PERUN_GROUP_ID = "perunGroupId"; + public static final String UNIQUE_MEMBER = "uniqueMember"; + + public static final String PERUN_VO_ID = "perunVoId"; + public static final String EDU_PERSON_PRINCIPAL_NAMES = "eduPersonPrincipalNames"; + + private final PerunConnectorLdap connectorLdap; + private final Map<String, AttributeMapping> attributeMappings; + + public PerunAdapterLdap(PerunConnectorLdap connectorLdap, + AttributeMappingProperties attributeMappingProperties) + { + this.connectorLdap = connectorLdap; + this.attributeMappings = attributeMappingProperties.getAttributeMappings(); + } + + @Override + public Long getPerunUserId(String extLogin, String extSourceName) { + if (!StringUtils.hasText(extLogin)) { + throw new IllegalArgumentException("Empty extLogin passed"); + } + // PARAM EXT_SOURCE_NAME IS NOT USED, THUS NOT VALIDATED + + FilterBuilder filter = and( + equal(OBJECT_CLASS, PERUN_USER), equal(EDU_PERSON_PRINCIPAL_NAMES, extLogin) + ); + + return getPerunUserId(filter); + } + + @Override + public boolean isUserInGroup(Long userId, Long groupId) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (groupId == null) { + throw new IllegalArgumentException("Null group ID passed"); + } + + String uniqueMemberValue = getUniqueMemberValue(userId); + + FilterBuilder filter = and( + equal(OBJECT_CLASS, PERUN_GROUP), + equal(PERUN_GROUP_ID, String.valueOf(groupId)), + equal(UNIQUE_MEMBER, uniqueMemberValue) + ); + + EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_GROUP_ID).getString()); + String[] attributes = new String[] { PERUN_GROUP_ID }; + List<Long> ids = connectorLdap.search(null, filter, SearchScope.SUBTREE, attributes, mapper); + + return ids.stream().filter(groupId::equals).count() == 1L; + } + + @Override + public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (!StringUtils.hasText(groupAffiliationsAttr)) { + throw new IllegalArgumentException("Empty group affiliations attribute name passed"); + } + + Set<Long> userGroupIds = getGroupIdsWhereUserIsMember(userId, null); + if (userGroupIds.isEmpty()) { + return new ArrayList<>(); + } + + FilterBuilder[] groupIdFilters = new FilterBuilder[userGroupIds.size()]; + int i = 0; + + for (Long id: userGroupIds) { + groupIdFilters[i++] = equal(PERUN_GROUP_ID, String.valueOf(id)); + } + + AttributeMapping affiliationsMapping = attributeMappings.get(groupAffiliationsAttr); + FilterBuilder filterBuilder = and(equal(OBJECT_CLASS, PERUN_GROUP), or(groupIdFilters)); + String[] attributes = new String[] { affiliationsMapping.getLdapName() }; + + EntryMapper<Set<Affiliation>> mapper = e -> { + Set<Affiliation> affiliations = new HashSet<>(); + if (!checkHasAttributes(e, attributes)) { + return affiliations; + } + + Attribute a = e.get(affiliationsMapping.getLdapName()); + long linuxTime = System.currentTimeMillis() / 1000L; + a.iterator().forEachRemaining(v -> affiliations.add( + new Affiliation(null, v.getString(), linuxTime))); + + return affiliations; + }; + + List<Set<Affiliation>> affiliationSets = connectorLdap.search( + null, filterBuilder, SearchScope.SUBTREE, attributes, mapper); + + return affiliationSets.stream().flatMap(Set::stream).distinct().collect(Collectors.toList()); + } + + @Override + public List<Affiliation> getGroupAffiliations(Long userId, Long voId, + String groupAffiliationsAttr) + { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (voId == null) { + throw new IllegalArgumentException("Null vo ID passed"); + } else if (!StringUtils.hasText(groupAffiliationsAttr)) { + throw new IllegalArgumentException("Empty group affiliations attribute name passed"); + } + + Set<Long> userGroupIds = getGroupIdsWhereUserIsMember(userId, voId); + if (userGroupIds.isEmpty()) { + return new ArrayList<>(); + } + + FilterBuilder[] groupIdFilters = new FilterBuilder[userGroupIds.size()]; + int i = 0; + + for (Long id: userGroupIds) { + groupIdFilters[i++] = equal(PERUN_GROUP_ID, String.valueOf(id)); + } + + AttributeMapping affiliationsMapping = attributeMappings.get(groupAffiliationsAttr); + FilterBuilder filterBuilder = and(equal(OBJECT_CLASS, PERUN_GROUP), or(groupIdFilters)); + String[] attributes = new String[] { affiliationsMapping.getLdapName() }; + + EntryMapper<Set<Affiliation>> mapper = e -> { + Set<Affiliation> affiliations = new HashSet<>(); + if (!checkHasAttributes(e, attributes)) { + return affiliations; + } + + Attribute a = e.get(affiliationsMapping.getLdapName()); + long linuxTime = System.currentTimeMillis() / 1000L; + a.iterator().forEachRemaining(v -> affiliations.add( + new Affiliation(null, v.getString(), linuxTime))); + + return affiliations; + }; + + List<Set<Affiliation>> affiliationSets = connectorLdap.search( + null, filterBuilder, SearchScope.SUBTREE, attributes, mapper); + + return affiliationSets.stream().flatMap(Set::stream).distinct().collect(Collectors.toList()); + } + + @Override + public Set<Long> getUserIdsByAttributeValue(String attrName, String attrValue) { + if (!StringUtils.hasText(attrName)) { + throw new IllegalArgumentException("Empty attribute name passed"); + } else if (!StringUtils.hasText(attrValue)) { + throw new IllegalArgumentException("Empty attribute value passed"); + } + + AttributeMapping attributeMapping = attributeMappings.getOrDefault(attrName, null); + if (attributeMapping == null) { + log.error("No LDAP mapping found for attribute '{}'", attrName); + return new HashSet<>(); + } + + FilterBuilder filter = and( + equal(OBJECT_CLASS, PERUN_USER), + equal(attributeMapping.getLdapName(), attrValue) + ); + + SearchScope scope = SearchScope.ONELEVEL; + String[] attributes = new String[]{ PERUN_USER_ID }; + EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_USER_ID).getString()); + + List<Long> result = connectorLdap.search(OU_PEOPLE, filter, scope, attributes, mapper); + + return Set.copyOf(result); + } + + @Override + public String getUserSub(Long userId, String subAttribute) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (!StringUtils.hasText(subAttribute)) { + throw new IllegalArgumentException("Empty attribute name passed"); + } + AttributeMapping attributeMapping = getAttributeMapping(subAttribute); + if (attributeMapping == null) { + return null; + } + + FilterBuilder filter = and( + equal(OBJECT_CLASS, PERUN_USER), + equal(PERUN_USER_ID, String.valueOf(userId)) + ); + + SearchScope scope = SearchScope.SUBTREE; + String[] attributes = new String[]{ attributeMapping.getLdapName() }; + EntryMapper<String> mapper = e -> e.get(attributeMapping.getLdapName()).getString(); + + return connectorLdap.searchFirst(OU_PEOPLE, filter, scope, attributes, mapper); + } + + @Override + public boolean isUserInVo(Long userId, Long voId) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (voId == null) { + throw new IllegalArgumentException("Null vo ID passed"); + } + + String uniqueMemberValue = getUniqueMemberValue(userId); + + FilterBuilder filter = and( + equal(OBJECT_CLASS, PERUN_VO), + equal(PERUN_VO_ID, String.valueOf(voId)), + equal(UNIQUE_MEMBER, uniqueMemberValue) + ); + + EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_VO_ID).getString()); + String[] attributes = new String[] { PERUN_VO_ID }; + List<Long> ids = connectorLdap.search(null, filter, SearchScope.SUBTREE, attributes, mapper); + + return ids.stream().filter(voId::equals).count() == 1L; + } + + private Set<Long> getGroupIdsWhereUserIsMember(Long userId, Long voId) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } + + String dnPrefix = getDnPrefixForUserId(userId); + String[] attributes = new String[] { MEMBER_OF }; + + EntryMapper<Set<Long>> mapper = e -> { + Set<Long> ids = new HashSet<>(); + if (checkHasAttributes(e, attributes)) { + Attribute a = e.get(MEMBER_OF); + a.iterator().forEachRemaining(id -> { + String fullVal = id.getString(); + String[] parts = fullVal.split(",", 3); + + String groupId = parts[0]; + groupId = groupId.replace(PERUN_GROUP_ID + '=', ""); + + String voIdStr = parts[1]; + voIdStr = voIdStr.replace(PERUN_VO_ID + '=', ""); + + if (voId == null || voId.equals(Long.parseLong(voIdStr))) { + ids.add(Long.parseLong(groupId)); + } + }); + } + + return ids; + }; + + Set<Long> res = connectorLdap.lookup(dnPrefix, attributes, mapper); + if (res == null) { + res = new HashSet<>(); + } + + return res; + } + + private String getDnPrefixForUserId(Long userId) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } + return PERUN_USER_ID + '=' + userId + ',' + OU_PEOPLE; + } + + private boolean checkHasAttributes(Entry e, String[] attributes) { + if (e == null) { + return false; + } else if (attributes == null) { + return true; + } + + for (String attr: attributes) { + if (e.get(attr) == null) { + return false; + } + } + + return true; + } + + private Long getPerunUserId(FilterBuilder filter) { + SearchScope scope = SearchScope.ONELEVEL; + String[] attributes = new String[]{PERUN_USER_ID}; + EntryMapper<Long> mapper = e -> Long.parseLong(e.get(PERUN_USER_ID).getString()); + + return connectorLdap.searchFirst(OU_PEOPLE, filter, scope, attributes, mapper); + } + + private AttributeMapping getAttributeMapping(String attribute) { + if (!StringUtils.hasText(attribute)) { + throw new IllegalArgumentException("Empty attribute name passed"); + } + + AttributeMapping mapping = attributeMappings.getOrDefault(attribute, null); + if (mapping == null) { + log.warn("No attribute mapping found for attribute '{}'", attribute); + return null; + } else if (!StringUtils.hasText(mapping.getLdapName())) { + log.warn("No LDAP name found in mapping for attribute '{}'", attribute); + return null; + } else { + return mapping; + } + } + + private String getUniqueMemberValue(Long userId) { + return PERUN_USER_ID + '=' + userId + ',' + OU_PEOPLE + ',' + connectorLdap.getBaseDN(); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterRpc.java b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterRpc.java new file mode 100644 index 0000000..4afec68 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/adapters/impl/PerunAdapterRpc.java @@ -0,0 +1,480 @@ +package cz.muni.ics.ga4gh.base.adapters.impl; + +import static cz.muni.ics.ga4gh.base.enums.MemberStatus.VALID; + +import com.fasterxml.jackson.databind.JsonNode; +import cz.muni.ics.ga4gh.base.adapters.PerunAdapterMethods; +import cz.muni.ics.ga4gh.base.adapters.PerunAdapterMethodsRpc; +import cz.muni.ics.ga4gh.base.adapters.PerunRpcAdapterMapper; +import cz.muni.ics.ga4gh.base.connectors.PerunConnectorRpc; +import cz.muni.ics.ga4gh.base.model.Affiliation; +import cz.muni.ics.ga4gh.base.model.AttributeMapping; +import cz.muni.ics.ga4gh.base.model.Group; +import cz.muni.ics.ga4gh.base.model.Member; +import cz.muni.ics.ga4gh.base.model.PerunAttributeValue; +import cz.muni.ics.ga4gh.base.model.UserExtSource; +import cz.muni.ics.ga4gh.base.properties.AttributeMappingProperties; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +@Slf4j +public class PerunAdapterRpc implements PerunAdapterMethods, PerunAdapterMethodsRpc { + + public static final String EXT_SOURCE_IDP = "cz.metacentrum.perun.core.impl.ExtSourceIdp"; + public static final String EXT_LOGIN = "extLogin"; + public static final String EXT_SOURCE_NAME = "extSourceName"; + public static final String USER_EXT_SOURCE = "userExtSource"; + public static final String ATTR_NAMES = "attrNames"; + public static final String VALUE_CREATED_AT = "valueCreatedAt"; + + public static final String ID = "id"; + public static final String VO = "vo"; + public static final String USER = "user"; + public static final String MEMBER = "member"; + public static final String GROUP = "group"; + + public static final String ATTRIBUTE_NAME = "attributeName"; + public static final String ATTRIBUTE_VALUE = "attributeValue"; + + public static final String ATTRIBUTES_MANAGER = "attributesManager"; + public static final String GROUPS_MANAGER = "groupsManager"; + public static final String MEMBERS_MANAGER = "membersManager"; + public static final String USERS_MANAGER = "usersManager"; + + public static final String GET_USER_BY_EXT_SOURCE_NAME_AND_EXT_LOGIN = "getUserByExtSourceNameAndExtLogin"; + public static final String GET_USER_EXT_SOURCES = "getUserExtSources"; + public static final String GET_GROUP_BY_ID = "getGroupById"; + public static final String GET_MEMBER_BY_USER = "getMemberByUser"; + public static final String GET_MEMBERS_BY_USER = "getMembersByUser"; + public static final String GET_MEMBER_GROUPS = "getMemberGroups"; + public static final String IS_GROUP_MEMBER = "isGroupMember"; + public static final String GET_ATTRIBUTE = "getAttribute"; + public static final String GET_ATTRIBUTES = "getAttributes"; + public static final String GET_USERS_BY_ATTRIBUTE_VALUE = "getUsersByAttributeValue"; + + private final PerunConnectorRpc connectorRpc; + private final Map<String, AttributeMapping> attributeMappings; + + public PerunAdapterRpc(PerunConnectorRpc connectorRpc, + AttributeMappingProperties attributeMappingProperties) + { + this.connectorRpc = connectorRpc; + this.attributeMappings = attributeMappingProperties.getAttributeMappings(); + } + + @Override + public Long getPerunUserId(String extLogin, String extSourceName) { + if (!StringUtils.hasText(extLogin)) { + throw new IllegalArgumentException("Empty extLogin passed"); + } else if (!StringUtils.hasText(extSourceName)) { + throw new IllegalArgumentException("Empty extSourceName passed"); + } + + if (!connectorRpc.isEnabled()) { + return null; + } + + Map<String, Object> map = new LinkedHashMap<>(); + map.put(EXT_LOGIN, extLogin); + map.put(EXT_SOURCE_NAME, extSourceName); + + JsonNode response = connectorRpc.post(USERS_MANAGER, GET_USER_BY_EXT_SOURCE_NAME_AND_EXT_LOGIN, map); + + return response.get(ID) == null ? null : response.get(ID).asLong(); + } + + @Override + public boolean isUserInGroup(Long userId, Long groupId) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (groupId == null) { + throw new IllegalArgumentException("Null group ID passed"); + } + + if (!connectorRpc.isEnabled()) { + return false; + } + + Map<String, Object> groupParams = new LinkedHashMap<>(); + groupParams.put(ID, groupId); + JsonNode groupResponse = connectorRpc.post(GROUPS_MANAGER, GET_GROUP_BY_ID, groupParams); + Group group = PerunRpcAdapterMapper.mapGroup(groupResponse); + + Map<String, Object> memberParams = new LinkedHashMap<>(); + memberParams.put(VO, group.getVoId()); + memberParams.put(USER, userId); + JsonNode memberResponse = connectorRpc.post(MEMBERS_MANAGER, GET_MEMBER_BY_USER, memberParams); + Member member = PerunRpcAdapterMapper.mapMember(memberResponse); + + Map<String, Object> isGroupMemberParams = new LinkedHashMap<>(); + isGroupMemberParams.put(GROUP, groupId); + isGroupMemberParams.put(MEMBER, member.getId()); + JsonNode res = connectorRpc.post(GROUPS_MANAGER, IS_GROUP_MEMBER, isGroupMemberParams); + + return res.asBoolean(false); + } + + @Override + public List<Affiliation> getGroupAffiliations(Long userId, String groupAffiliationsAttr) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (!StringUtils.hasText(groupAffiliationsAttr)) { + throw new IllegalArgumentException("Empty group affiliations attribute name passed"); + } + if (!connectorRpc.isEnabled()) { + return new ArrayList<>(); + } + + List<Affiliation> affiliations = new ArrayList<>(); + List<Member> userMembers = getMembersByUser(userId); + + for (Member member : userMembers) { + if (!VALID.equals(member.getStatus())) { + continue; + } + List<Group> memberGroups = getMemberGroups(member.getId()); + for (Group group : memberGroups) { + PerunAttributeValue attrValue = getGroupAttributeValue(group, groupAffiliationsAttr); + if (attrValue != null && attrValue.valueAsList() != null) { + long linuxTime = System.currentTimeMillis() / 1000L; + + for (String value : attrValue.valueAsList()) { + Affiliation affiliation = new Affiliation(null, value, linuxTime); + log.debug("found {} on group {}", value, group.getName()); + affiliations.add(affiliation); + } + } + } + } + + return affiliations; + } + + @Override + public List<Affiliation> getGroupAffiliations(Long userId, Long voId, + String groupAffiliationsAttr) + { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (voId == null) { + throw new IllegalArgumentException("Null vo ID passed"); + } else if (!StringUtils.hasText(groupAffiliationsAttr)) { + throw new IllegalArgumentException("Empty group affiliations attribute name passed"); + } + if (!connectorRpc.isEnabled()) { + return new ArrayList<>(); + } + + List<Affiliation> affiliations = new ArrayList<>(); + List<Member> userMembers = getMembersByUser(userId); + + for (Member member : userMembers) { + if (!VALID.equals(member.getStatus())) { + continue; + } else if (!Objects.equals(voId, member.getVoId())) { + continue; + } + List<Group> memberGroups = getMemberGroups(member.getId()); + for (Group group : memberGroups) { + PerunAttributeValue attrValue = getGroupAttributeValue(group, groupAffiliationsAttr); + if (attrValue != null && attrValue.valueAsList() != null) { + long linuxTime = System.currentTimeMillis() / 1000L; + + for (String value : attrValue.valueAsList()) { + Affiliation affiliation = new Affiliation(null, value, linuxTime); + log.debug("found {} on group {}", value, group.getName()); + affiliations.add(affiliation); + } + } + } + } + + return affiliations; + } + + @Override + public String getUserAttributeCreatedAt(Long userId, String attrName) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (!StringUtils.hasText(attrName)) { + throw new IllegalArgumentException("Empty attribute name passed"); + } + if (!connectorRpc.isEnabled()) { + return null; + } + + AttributeMapping mapping = getAttributeMapping(attrName); + if (mapping == null) { + return null; + } + + Map<String, Object> map = new LinkedHashMap<>(); + map.put(USER, userId); + map.put(ATTRIBUTE_NAME, mapping.getRpcName()); + + JsonNode res = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTE, map); + if (res == null || !res.hasNonNull(VALUE_CREATED_AT)) { + return null; + } + return res.get(VALUE_CREATED_AT).asText(); + } + + @Override + public List<Affiliation> getUserExtSourcesAffiliations(Long userId, + String affiliationsAttr, + String orgUrlAttr) + { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (!StringUtils.hasText(affiliationsAttr)) { + throw new IllegalArgumentException("Empty affiliations attr name passed"); + } else if (!StringUtils.hasText(orgUrlAttr)) { + throw new IllegalArgumentException("Empty organizationURL attr name passed"); + } + + if (!connectorRpc.isEnabled()) { + return new ArrayList<>(); + } + + List<UserExtSource> userExtSources = getUserExtSources(userId); + if (userExtSources.isEmpty()) { + return new ArrayList<>(); + } + + List<Affiliation> affiliations = new ArrayList<>(); + for (UserExtSource ues : userExtSources) { + if (!EXT_SOURCE_IDP.equals(ues.getExtSource().getType())) { + continue; + } + Map<String, PerunAttributeValue> uesAttrValues = getUserExtSourceAttributeValues( + ues.getId(), Arrays.asList(affiliationsAttr, orgUrlAttr)); + + long asserted = ues.getLastAccess().getTime() / 1000L; + + String affs = uesAttrValues.get(affiliationsAttr).valueAsString(); + String orgUrl = uesAttrValues.get(orgUrlAttr).valueAsString(); + + if (affs != null) { + for (String aff : affs.split(";")) { + String source = ( (orgUrl != null) ? orgUrl : ues.getExtSource().getName() ); + Affiliation affiliation = new Affiliation(source, aff, asserted); + log.debug("found {} from IdP {} with orgURL {} asserted at {}", aff, ues.getExtSource().getName(), + orgUrl, asserted); + affiliations.add(affiliation); + } + } + } + + return affiliations; + } + + @Override + public Set<Long> getUserIdsByAttributeValue(String attrName, String attrValue) { + if (!StringUtils.hasText(attrName)) { + throw new IllegalArgumentException("Empty attribute name passed"); + } else if (!StringUtils.hasText(attrValue)) { + throw new IllegalArgumentException("Empty attribute value passed"); + } + + if (!connectorRpc.isEnabled()) { + return new HashSet<>(); + } + + AttributeMapping mapping = getAttributeMapping(attrName); + if (mapping == null) { + return new HashSet<>(); + } + + Set<Long> result = new HashSet<>(); + Map<String, Object> map = new LinkedHashMap<>(); + map.put(ATTRIBUTE_NAME, mapping.getRpcName()); + map.put(ATTRIBUTE_VALUE, attrValue); + + JsonNode res = connectorRpc.post(USERS_MANAGER, GET_USERS_BY_ATTRIBUTE_VALUE, map); + if (res != null) { + for (int i = 0; i < res.size(); i++) { + result.add(res.get(i).get(ID).asLong()); + } + } + return result; + } + + @Override + public String getUserSub(Long userId, String subAttribute) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (!StringUtils.hasText(subAttribute)) { + throw new IllegalArgumentException("Empty sub attribute name passed"); + } + if (!connectorRpc.isEnabled()) { + return null; + } + + AttributeMapping mapping = getAttributeMapping(subAttribute); + if (mapping == null) { + return null; + } + + Map<String, Object> map = new LinkedHashMap<>(); + map.put(USER, userId); + map.put(ATTRIBUTE_NAME, mapping.getRpcName()); + JsonNode res = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTE, map); + + PerunAttributeValue value = PerunRpcAdapterMapper.mapAttributeValue(res); + if (value != null) { + return value.getValue().textValue(); + } else { + return null; + } + } + @Override + public boolean isUserInVo(Long userId, Long voId) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } else if (voId == null) { + throw new IllegalArgumentException("Null vo ID passed"); + } + + if (!connectorRpc.isEnabled()) { + return false; + } + + Map<String, Object> memberParams = new LinkedHashMap<>(); + memberParams.put(VO, voId); + memberParams.put(USER, userId); + JsonNode memberResponse = connectorRpc.post(MEMBERS_MANAGER, GET_MEMBER_BY_USER, memberParams); + Member member = PerunRpcAdapterMapper.mapMember(memberResponse); + + return member != null && member.getStatus() == VALID; + } + + @Override + public List<UserExtSource> getIdpUserExtSources(Long userId) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } + + List<UserExtSource> userExtSources = getUserExtSources(userId); + if (userExtSources.isEmpty()) { + return new ArrayList<>(); + } + userExtSources = userExtSources.stream() + .filter(ues -> EXT_SOURCE_IDP.equals(ues.getExtSource().getType())) + .collect(Collectors.toList()); + return userExtSources; + } + + private List<Member> getMembersByUser(Long userId) { + if (!this.connectorRpc.isEnabled()) { + return new ArrayList<>(); + } + + Map<String, Object> params = new LinkedHashMap<>(); + params.put(USER, userId); + JsonNode jsonNode = connectorRpc.post(MEMBERS_MANAGER, GET_MEMBERS_BY_USER, params); + + return PerunRpcAdapterMapper.mapMembers(jsonNode); + } + + private List<Group> getMemberGroups(Long memberId) { + if (memberId == null) { + throw new IllegalArgumentException("Null member ID passed"); + } + if (!this.connectorRpc.isEnabled()) { + return new ArrayList<>(); + } + + Map<String, Object> map = new LinkedHashMap<>(); + map.put(MEMBER, memberId); + + JsonNode response = connectorRpc.post(GROUPS_MANAGER, GET_MEMBER_GROUPS, map); + return PerunRpcAdapterMapper.mapGroups(response); + } + + private PerunAttributeValue getGroupAttributeValue(Group group, String attrToFetch) { + if (group == null || group.getId() == null) { + throw new IllegalArgumentException("Null group or group with null ID passed"); + } else if (!StringUtils.hasText(attrToFetch)) { + throw new IllegalArgumentException("Empty attribute name passed"); + } + AttributeMapping attributeMapping = getAttributeMapping(attrToFetch); + if (attributeMapping == null) { + return null; + } + + Map<String, Object> map = new LinkedHashMap<>(); + map.put(GROUP, group.getId()); + map.put(ATTRIBUTE_NAME, attributeMapping.getRpcName()); + JsonNode res = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTE, map); + + return PerunRpcAdapterMapper.mapAttributeValue(res); + } + + private List<UserExtSource> getUserExtSources(Long userId) { + if (userId == null) { + throw new IllegalArgumentException("Null user ID passed"); + } + Map<String, Object> map = new LinkedHashMap<>(); + map.put(USER, userId); + + JsonNode response = connectorRpc.post(USERS_MANAGER, GET_USER_EXT_SOURCES, map); + return PerunRpcAdapterMapper.mapUserExtSources(response); + } + + private Map<String, PerunAttributeValue> getUserExtSourceAttributeValues(Long uesId, + List<String> attributeNames) + { + if (uesId == null) { + throw new IllegalArgumentException("Null user ext source ID passed"); + } else if (attributeNames == null || attributeNames.isEmpty()) { + throw new IllegalArgumentException("Null or empty list of attribute names passed"); + } + + Map<String, Object> map = new LinkedHashMap<>(); + + Map<String, AttributeMapping> mappings = new HashMap<>(); + for (String attrName: attributeNames) { + AttributeMapping mapping = getAttributeMapping(attrName); + if (mapping != null) { + mappings.put(attrName, mapping); + } + } + + map.put(USER_EXT_SOURCE, uesId); + map.put(ATTR_NAMES, mappings.values().stream() + .map(AttributeMapping::getRpcName) + .collect(Collectors.toList())); + + JsonNode response = connectorRpc.post(ATTRIBUTES_MANAGER, GET_ATTRIBUTES, map); + + return PerunRpcAdapterMapper.mapAttributes(response, mappings); + } + + private AttributeMapping getAttributeMapping(String attribute) { + if (!StringUtils.hasText(attribute)) { + throw new IllegalArgumentException("Empty attribute name passed"); + } + + AttributeMapping mapping = attributeMappings.getOrDefault(attribute, null); + if (mapping == null) { + log.warn("No attribute mapping found for attribute '{}'", attribute); + return null; + } else if (!StringUtils.hasText(mapping.getRpcName())) { + log.warn("No RPC name found in mapping for attribute '{}'", attribute); + return null; + } else { + return mapping; + } + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorLdap.java b/src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorLdap.java similarity index 66% rename from src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorLdap.java rename to src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorLdap.java index 5f4c66f..fbebb58 100644 --- a/src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorLdap.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorLdap.java @@ -1,7 +1,7 @@ -package cz.muni.ics.ga4gh.connectors; +package cz.muni.ics.ga4gh.base.connectors; -import cz.muni.ics.ga4gh.aop.LogTimes; -import cz.muni.ics.ga4gh.config.LdapConfig; +import cz.muni.ics.ga4gh.base.properties.PerunLdapConnectorProperties; +import java.util.List; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; @@ -17,69 +17,58 @@ import org.apache.directory.ldap.client.template.EntryMapper; import org.apache.directory.ldap.client.template.LdapConnectionTemplate; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Repository; import org.springframework.util.StringUtils; -import java.util.List; -import java.util.Objects; - -@Repository @Slf4j -@Getter public class PerunConnectorLdap implements DisposableBean { + @Getter private final String baseDN; private final LdapConnectionPool pool; private final LdapConnectionTemplate ldap; @Autowired - public PerunConnectorLdap(LdapConfig config) { - if (config.getHost() == null || config.getHost().trim().isEmpty()) { + public PerunConnectorLdap(PerunLdapConnectorProperties perunLdapConnectorProperties) { + if (!StringUtils.hasText(perunLdapConnectorProperties.getHost())) { throw new IllegalArgumentException("Host cannot be null or empty"); - } else if (config.getBaseDn() == null || config.getBaseDn().trim().isEmpty()) { + } else if (!StringUtils.hasText(perunLdapConnectorProperties.getBaseDn())) { throw new IllegalArgumentException("baseDN cannot be null or empty"); } - boolean useTLS = Objects.requireNonNullElse(config.getUseTls(), false); - boolean useSSL = Objects.requireNonNullElse(config.getUseSsl(), false); - boolean allowUntrustedSsl = Objects.requireNonNullElse(config.getAllowUntrustedSsl(), false); - long timeoutSecs = Objects.requireNonNullElse(config.getTimeoutSecs(), 5L); + this.baseDN = perunLdapConnectorProperties.getBaseDn(); - - this.baseDN = config.getBaseDn(); - - LdapConnectionConfig ldapConnectionConfig = getLdapConnectionConfig(config.getHost(), config.getPort(), useTLS, useSSL, allowUntrustedSsl); - if (config.getUser() != null && !config.getUser().isEmpty()) { - log.debug("setting ldap user to {}", config.getUser()); - ldapConnectionConfig.setName(config.getUser()); + LdapConnectionConfig ldapConnectionConfig = getLdapConnectionConfig( + perunLdapConnectorProperties); + if (StringUtils.hasText(perunLdapConnectorProperties.getUser())) { + log.debug("Setting ldap user to '{}'", perunLdapConnectorProperties.getUser()); + ldapConnectionConfig.setName(perunLdapConnectorProperties.getUser()); } - if (config.getPassword() != null && !config.getPassword().isEmpty()) { - log.debug("setting ldap password"); - ldapConnectionConfig.setCredentials(config.getPassword()); + + if (StringUtils.hasText(perunLdapConnectorProperties.getPassword())) { + log.debug("Setting ldap password"); + ldapConnectionConfig.setCredentials(perunLdapConnectorProperties.getPassword()); } + DefaultLdapConnectionFactory factory = new DefaultLdapConnectionFactory(ldapConnectionConfig); - factory.setTimeOut(timeoutSecs * 1000L); + factory.setTimeOut(perunLdapConnectorProperties.getTimeoutSecs() * 1000L); GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); poolConfig.setTestOnBorrow(true); - pool = new LdapConnectionPool(new DefaultPoolableLdapConnectionFactory(factory), poolConfig); - ldap = new LdapConnectionTemplate(pool); + this.pool = new LdapConnectionPool(new DefaultPoolableLdapConnectionFactory(factory), poolConfig); + this.ldap = new LdapConnectionTemplate(pool); log.debug("initialized LDAP connector"); } - public String getBaseDN() { - return baseDN; - } - - private LdapConnectionConfig getLdapConnectionConfig(String host, int port, boolean useTLS, boolean useSSL, - boolean allowUntrustedSsl) { + private LdapConnectionConfig getLdapConnectionConfig( + PerunLdapConnectorProperties perunLdapConnectorProperties) + { LdapConnectionConfig config = new LdapConnectionConfig(); - config.setLdapHost(host); - config.setLdapPort(port); - config.setUseSsl(useSSL); - config.setUseTls(useTLS); - if (allowUntrustedSsl) { + config.setLdapHost(perunLdapConnectorProperties.getHost()); + config.setLdapPort(perunLdapConnectorProperties.getPort()); + config.setUseSsl(perunLdapConnectorProperties.isUseSsl()); + config.setUseTls(perunLdapConnectorProperties.isUseTls()); + if (perunLdapConnectorProperties.isAllowUntrustedSsl()) { config.setTrustManagers(new NoVerificationTrustManager()); } @@ -98,12 +87,11 @@ public class PerunConnectorLdap implements DisposableBean { * @param dnPrefix Prefix to be added to the base DN. (i.e. ou=People) !DO NOT END WITH A COMMA! * @param filter Filter for entries * @param scope Search scope - * @param attributes Attributes to be fetch for entry + * @param attributes Attributes to be fetched for entry * @param entryMapper Mapper of entries to the target class T * @param <T> Class that the result should be mapped to. * @return Found entry mapped to target class */ - @LogTimes public <T> T searchFirst(String dnPrefix, FilterBuilder filter, SearchScope scope, String[] attributes, EntryMapper<T> entryMapper) { @@ -114,12 +102,11 @@ public class PerunConnectorLdap implements DisposableBean { /** * Perform lookup for the entry that satisfies criteria. * @param dnPrefix Prefix to be added to the base DN. (i.e. ou=People) !DO NOT END WITH A COMMA! - * @param attributes Attributes to be fetch for entry + * @param attributes Attributes to be fetched for entry * @param entryMapper Mapper of entries to the target class T * @param <T> Class that the result should be mapped to. * @return Found entry mapped to target class */ - @LogTimes public <T> T lookup(String dnPrefix, String[] attributes, EntryMapper<T> entryMapper) { Dn fullDn = getFullDn(dnPrefix); return ldap.lookup(fullDn, attributes, entryMapper); @@ -130,12 +117,11 @@ public class PerunConnectorLdap implements DisposableBean { * @param dnPrefix Prefix to be added to the base DN. (i.e. ou=People) !DO NOT END WITH A COMMA! * @param filter Filter for entries * @param scope Search scope - * @param attributes Attributes to be fetch for entry + * @param attributes Attributes to be fetched for entry * @param entryMapper Mapper of entries to the target class T * @param <T> Class that the result should be mapped to. * @return List of found entries mapped to target class */ - @LogTimes public <T> List<T> search(String dnPrefix, FilterBuilder filter, SearchScope scope, String[] attributes, EntryMapper<T> entryMapper) { @@ -151,4 +137,5 @@ public class PerunConnectorLdap implements DisposableBean { return ldap.newDn(dn); } + } diff --git a/src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorRpc.java b/src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorRpc.java similarity index 56% rename from src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorRpc.java rename to src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorRpc.java index ecacbf7..f5d9772 100644 --- a/src/main/java/cz/muni/ics/ga4gh/connectors/PerunConnectorRpc.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/connectors/PerunConnectorRpc.java @@ -1,10 +1,15 @@ -package cz.muni.ics.ga4gh.connectors; +package cz.muni.ics.ga4gh.base.connectors; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; -import cz.muni.ics.ga4gh.aop.LogTimes; -import cz.muni.ics.ga4gh.config.RpcConfig; +import cz.muni.ics.ga4gh.base.properties.PerunRpcConnectorProperties; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.apache.http.HeaderElement; @@ -21,96 +26,67 @@ import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.InterceptingClientHttpRequestFactory; -import org.springframework.http.client.support.BasicAuthorizationInterceptor; -import org.springframework.stereotype.Repository; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; -import javax.annotation.PostConstruct; -import java.io.IOException; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -@Repository -@Slf4j @Getter +@Slf4j +@Validated public class PerunConnectorRpc { - public static final String ATTRIBUTES_MANAGER = "attributesManager"; - public static final String FACILITIES_MANAGER = "facilitiesManager"; - public static final String GROUPS_MANAGER = "groupsManager"; - public static final String MEMBERS_MANAGER = "membersManager"; - public static final String REGISTRAR_MANAGER = "registrarManager"; - public static final String SEARCHER = "searcher"; - public static final String USERS_MANAGER = "usersManager"; - public static final String VOS_MANAGER = "vosManager"; - public static final String RESOURCES_MANAGER = "resourcesManager"; - - private String perunUrl; - private String perunUser; - private String perunPassword; - private boolean isEnabled; - private String serializer; - private RestTemplate restTemplate; + @NotBlank + private final String url; - @Autowired - public PerunConnectorRpc(RpcConfig config) { - this.setPerunUrl(config.getUrl()); - this.setPerunUser(config.getUsername()); - this.setPerunPassword(config.getPassword()); - this.setEnabled(config.getEnabled()); - this.setSerializer(config.getSerializer()); - } + private final boolean enabled; - public void setPerunUrl(String perunUrl) { - if (!StringUtils.hasText(perunUrl)) { - throw new IllegalArgumentException("Perun URL cannot be null or empty"); - } else if (perunUrl.endsWith("/")) { - perunUrl = perunUrl.substring(0, perunUrl.length() - 1); - } + @NotBlank + private final String serializer; - this.perunUrl = perunUrl; - } + @NotNull + private RestTemplate restTemplate; - public void setPerunUser(String perunUser) { - if (!StringUtils.hasText(perunUser)) { - throw new IllegalArgumentException("Perun USER cannot be null or empty"); - } + private final long connectionTimeout; + private final long connectionRequestTimeout; + private final long requestTimeout; - this.perunUser = perunUser; + @Autowired + public PerunConnectorRpc(PerunRpcConnectorProperties rpcProperties) { + this.url = setUrl(rpcProperties.getUrl()); + this.enabled = rpcProperties.isEnabled(); + this.serializer = setSerializer(rpcProperties.getSerializer()); + this.connectionTimeout = rpcProperties.getConnectionTimeout(); + this.connectionRequestTimeout = rpcProperties.getConnectionRequestTimeout(); + this.requestTimeout = rpcProperties.getRequestTimeout(); + initRestTemplate(rpcProperties); } - public void setPerunPassword(String perunPassword) { - if (!StringUtils.hasText(perunPassword)) { - throw new IllegalArgumentException("Perun PASSWORD cannot be null or empty"); + private String setUrl(String url) { + if (!StringUtils.hasText(url)) { + throw new IllegalArgumentException("Perun URL cannot be null or empty"); + } else if (url.endsWith("/")) { + url = url.substring(0, url.length() - 1); } - this.perunPassword = perunPassword; + return url; } - public void setEnabled(Boolean enabled) { - this.isEnabled = Objects.requireNonNullElse(enabled, false); - } - - public void setSerializer(String serializer) { + private String setSerializer(String serializer) { if (!StringUtils.hasText(serializer)) { - this.serializer = "json"; + serializer = "json"; } - - this.serializer = serializer; + return serializer; } - @PostConstruct - public void postInit() { + public void initRestTemplate(PerunRpcConnectorProperties rpcProperties) { restTemplate = new RestTemplate(); //HTTP connection pooling, see https://howtodoinjava.com/spring-restful/resttemplate-httpclient-java-config/ RequestConfig requestConfig = RequestConfig.custom() - .setConnectionRequestTimeout(30000) // The timeout when requesting a connection from the connection manager - .setConnectTimeout(30000) // Determines the timeout in milliseconds until a connection is established - .setSocketTimeout(60000) // The timeout for waiting for data + .setConnectionRequestTimeout(rpcProperties.getConnectionRequestTimeout()) // The timeout when requesting a connection from the connection manager + .setConnectTimeout(rpcProperties.getConnectionTimeout()) // Determines the timeout in milliseconds until a connection is established + .setSocketTimeout(rpcProperties.getRequestTimeout()) // The timeout for waiting for data .build(); PoolingHttpClientConnectionManager poolingConnectionManager = new PoolingHttpClientConnectionManager(); @@ -118,8 +94,8 @@ public class PerunConnectorRpc { poolingConnectionManager.setDefaultMaxPerRoute(18); ConnectionKeepAliveStrategy connectionKeepAliveStrategy = (response, context) -> { - HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE)); - + HeaderElementIterator it = new BasicHeaderElementIterator( + response.headerIterator(HTTP.CONN_KEEP_ALIVE)); while (it.hasNext()) { HeaderElement he = it.nextElement(); String param = he.getName(); @@ -142,8 +118,11 @@ public class PerunConnectorRpc { HttpComponentsClientHttpRequestFactory poolingRequestFactory = new HttpComponentsClientHttpRequestFactory(); poolingRequestFactory.setHttpClient(httpClient); //basic authentication - List<ClientHttpRequestInterceptor> interceptors = - Collections.singletonList(new BasicAuthorizationInterceptor(perunUser, perunPassword)); + List<ClientHttpRequestInterceptor> interceptors = Collections.singletonList( + new BasicAuthenticationInterceptor( + rpcProperties.getUsername(), + rpcProperties.getPassword() + )); InterceptingClientHttpRequestFactory authenticatingRequestFactory = new InterceptingClientHttpRequestFactory(poolingRequestFactory, interceptors); restTemplate.setRequestFactory(authenticatingRequestFactory); } @@ -155,26 +134,29 @@ public class PerunConnectorRpc { * @param map Map of parameters to be passed as request body * @return Response from Perun */ - @LogTimes public JsonNode post(String manager, String method, Map<String, Object> map) { - if (!this.isEnabled) { + if (!this.enabled) { return JsonNodeFactory.instance.nullNode(); } - String actionUrl = perunUrl + '/' + serializer + '/' + manager + '/' + method; + String actionUrl = url + '/' + serializer + '/' + manager + '/' + method; //make the call try { - log.debug("calling {} with {}", actionUrl, map); - + log.debug("Calling Perun - URL '{}' with parameters '{}'", actionUrl, map); return restTemplate.postForObject(actionUrl, map, JsonNode.class); } catch (HttpClientErrorException ex) { - MediaType contentType = ex.getResponseHeaders().getContentType(); + MediaType contentType = null; + if (ex.getResponseHeaders() != null) { + contentType = ex.getResponseHeaders().getContentType(); + } String body = ex.getResponseBodyAsString(); - log.error("HTTP ERROR " + ex.getRawStatusCode() + " URL " + actionUrl + " Content-Type: " + contentType); + log.error("HTTP ERROR when calling Perun RPC - {}, {}, {}", + ex.getRawStatusCode(), contentType, actionUrl); - if ("json".equals(contentType.getSubtype())) { + if (contentType != null && "json".equals(contentType.getSubtype())) { try { - log.error(new ObjectMapper().readValue(body, JsonNode.class).path("message").asText()); + log.error(new ObjectMapper().readValue(body, JsonNode.class) + .path("message").asText()); } catch (IOException e) { log.error("cannot parse error message from JSON", e); } @@ -185,4 +167,5 @@ public class PerunConnectorRpc { throw new RuntimeException("cannot connect to Perun RPC", ex); } } + } diff --git a/src/main/java/cz/muni/ics/ga4gh/enums/MemberStatus.java b/src/main/java/cz/muni/ics/ga4gh/base/enums/MemberStatus.java similarity index 94% rename from src/main/java/cz/muni/ics/ga4gh/enums/MemberStatus.java rename to src/main/java/cz/muni/ics/ga4gh/base/enums/MemberStatus.java index 9794780..738bb8f 100644 --- a/src/main/java/cz/muni/ics/ga4gh/enums/MemberStatus.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/enums/MemberStatus.java @@ -1,4 +1,4 @@ -package cz.muni.ics.ga4gh.enums; +package cz.muni.ics.ga4gh.base.enums; public enum MemberStatus { diff --git a/src/main/java/cz/muni/ics/ga4gh/base/exceptions/ConfigurationException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/ConfigurationException.java new file mode 100644 index 0000000..dee3be1 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/ConfigurationException.java @@ -0,0 +1,26 @@ +package cz.muni.ics.ga4gh.base.exceptions; + +public class ConfigurationException extends Exception { + + public ConfigurationException() { + super(); + } + + public ConfigurationException(String message) { + super(message); + } + + public ConfigurationException(String message, Throwable cause) { + super(message, cause); + } + + public ConfigurationException(Throwable cause) { + super(cause); + } + + protected ConfigurationException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/exceptions/InconvertibleValueException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/InconvertibleValueException.java similarity index 92% rename from src/main/java/cz/muni/ics/ga4gh/exceptions/InconvertibleValueException.java rename to src/main/java/cz/muni/ics/ga4gh/base/exceptions/InconvertibleValueException.java index 5816702..33b6c76 100644 --- a/src/main/java/cz/muni/ics/ga4gh/exceptions/InconvertibleValueException.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/InconvertibleValueException.java @@ -1,4 +1,4 @@ -package cz.muni.ics.ga4gh.exceptions; +package cz.muni.ics.ga4gh.base.exceptions; public class InconvertibleValueException extends RuntimeException { diff --git a/src/main/java/cz/muni/ics/ga4gh/base/exceptions/InvalidRequestParametersException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/InvalidRequestParametersException.java new file mode 100644 index 0000000..fa562e6 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/InvalidRequestParametersException.java @@ -0,0 +1,26 @@ +package cz.muni.ics.ga4gh.base.exceptions; + +public class InvalidRequestParametersException extends Exception { + + public InvalidRequestParametersException() { + super(); + } + + public InvalidRequestParametersException(String message) { + super(message); + } + + public InvalidRequestParametersException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidRequestParametersException(Throwable cause) { + super(cause); + } + + protected InvalidRequestParametersException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/exceptions/MissingFieldException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/MissingFieldException.java similarity index 92% rename from src/main/java/cz/muni/ics/ga4gh/exceptions/MissingFieldException.java rename to src/main/java/cz/muni/ics/ga4gh/base/exceptions/MissingFieldException.java index a889a9b..d2e07ea 100644 --- a/src/main/java/cz/muni/ics/ga4gh/exceptions/MissingFieldException.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/MissingFieldException.java @@ -1,4 +1,4 @@ -package cz.muni.ics.ga4gh.exceptions; +package cz.muni.ics.ga4gh.base.exceptions; public class MissingFieldException extends RuntimeException { diff --git a/src/main/java/cz/muni/ics/ga4gh/base/exceptions/PerunAdapterOperationException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/PerunAdapterOperationException.java new file mode 100644 index 0000000..ea469d2 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/PerunAdapterOperationException.java @@ -0,0 +1,26 @@ +package cz.muni.ics.ga4gh.base.exceptions; + +public class PerunAdapterOperationException extends RuntimeException { + + public PerunAdapterOperationException() { + super(); + } + + public PerunAdapterOperationException(String message) { + super(message); + } + + public PerunAdapterOperationException(String message, Throwable cause) { + super(message, cause); + } + + public PerunAdapterOperationException(Throwable cause) { + super(cause); + } + + protected PerunAdapterOperationException(String message, Throwable cause, + boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotFoundException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotFoundException.java new file mode 100644 index 0000000..84df696 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotFoundException.java @@ -0,0 +1,26 @@ +package cz.muni.ics.ga4gh.base.exceptions; + +public class UserNotFoundException extends Exception { + + public UserNotFoundException() { + super(); + } + + public UserNotFoundException(String message) { + super(message); + } + + public UserNotFoundException(String message, Throwable cause) { + super(message, cause); + } + + public UserNotFoundException(Throwable cause) { + super(cause); + } + + protected UserNotFoundException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/exceptions/UserNotUniqueException.java b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotUniqueException.java similarity index 82% rename from src/main/java/cz/muni/ics/ga4gh/exceptions/UserNotUniqueException.java rename to src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotUniqueException.java index dc3eee0..5eb3222 100644 --- a/src/main/java/cz/muni/ics/ga4gh/exceptions/UserNotUniqueException.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/exceptions/UserNotUniqueException.java @@ -1,6 +1,6 @@ -package cz.muni.ics.ga4gh.exceptions; +package cz.muni.ics.ga4gh.base.exceptions; -public class UserNotUniqueException extends RuntimeException { +public class UserNotUniqueException extends Exception { public UserNotUniqueException() { super(); diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Affiliation.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Affiliation.java similarity index 63% rename from src/main/java/cz/muni/ics/ga4gh/model/Affiliation.java rename to src/main/java/cz/muni/ics/ga4gh/base/model/Affiliation.java index 767a472..ed21e07 100644 --- a/src/main/java/cz/muni/ics/ga4gh/model/Affiliation.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Affiliation.java @@ -1,19 +1,24 @@ -package cz.muni.ics.ga4gh.model; +package cz.muni.ics.ga4gh.base.model; +import javax.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; +import org.springframework.validation.annotation.Validated; @Getter @ToString @EqualsAndHashCode @AllArgsConstructor +@Validated public class Affiliation { private final String source; + @NotBlank private final String value; private final long asserted; + } diff --git a/src/main/java/cz/muni/ics/ga4gh/model/AttributeMapping.java b/src/main/java/cz/muni/ics/ga4gh/base/model/AttributeMapping.java similarity index 66% rename from src/main/java/cz/muni/ics/ga4gh/model/AttributeMapping.java rename to src/main/java/cz/muni/ics/ga4gh/base/model/AttributeMapping.java index c90f81f..1c190f7 100644 --- a/src/main/java/cz/muni/ics/ga4gh/model/AttributeMapping.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/AttributeMapping.java @@ -1,20 +1,22 @@ -package cz.muni.ics.ga4gh.model; +package cz.muni.ics.ga4gh.base.model; +import javax.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +import org.springframework.validation.annotation.Validated; @Getter @Setter -@NoArgsConstructor -@AllArgsConstructor @ToString @EqualsAndHashCode +@AllArgsConstructor +@Validated public class AttributeMapping { + @NotBlank private String internalName; private String rpcName; diff --git a/src/main/java/cz/muni/ics/ga4gh/base/model/BasicAuthCredentials.java b/src/main/java/cz/muni/ics/ga4gh/base/model/BasicAuthCredentials.java new file mode 100644 index 0000000..8525212 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/BasicAuthCredentials.java @@ -0,0 +1,28 @@ +package cz.muni.ics.ga4gh.base.model; + +import javax.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import org.springframework.validation.annotation.Validated; + +@Getter +@Setter +@EqualsAndHashCode +@AllArgsConstructor +@Validated +public class BasicAuthCredentials { + @NotBlank + private String username; + @NotBlank + private String password; + + @Override + public String toString() { + return "BasicAuthCredentials{" + + "username='" + username + '\'' + + ", password='PROTECTED_STRING'" + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/cz/muni/ics/ga4gh/model/RepoHeader.java b/src/main/java/cz/muni/ics/ga4gh/base/model/ClaimRepositoryHeader.java similarity index 59% rename from src/main/java/cz/muni/ics/ga4gh/model/RepoHeader.java rename to src/main/java/cz/muni/ics/ga4gh/base/model/ClaimRepositoryHeader.java index d8932c1..e36f878 100644 --- a/src/main/java/cz/muni/ics/ga4gh/model/RepoHeader.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/ClaimRepositoryHeader.java @@ -1,5 +1,7 @@ -package cz.muni.ics.ga4gh.model; +package cz.muni.ics.ga4gh.base.model; +import java.io.IOException; +import javax.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -9,22 +11,26 @@ import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; -import java.io.IOException; - @Getter @Setter @AllArgsConstructor @NoArgsConstructor -public class RepoHeader implements ClientHttpRequestInterceptor { +public class ClaimRepositoryHeader implements ClientHttpRequestInterceptor { + @NotBlank private String header; + @NotBlank private String value; @Override - public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + public ClientHttpResponse intercept(HttpRequest request, + byte[] body, + ClientHttpRequestExecution execution) + throws IOException + { request.getHeaders().add(header, value); - return execution.execute(request, body); } + } diff --git a/src/main/java/cz/muni/ics/ga4gh/base/model/ExtSource.java b/src/main/java/cz/muni/ics/ga4gh/base/model/ExtSource.java new file mode 100644 index 0000000..d37340f --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/ExtSource.java @@ -0,0 +1,28 @@ +package cz.muni.ics.ga4gh.base.model; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.validation.annotation.Validated; + +@Getter +@Setter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@Validated +public class ExtSource { + + @NotNull + private Long id; + + @NotBlank + private String name; + + @NotBlank + private String type; +} diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Ga4ghClaimRepository.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghClaimRepository.java similarity index 60% rename from src/main/java/cz/muni/ics/ga4gh/model/Ga4ghClaimRepository.java rename to src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghClaimRepository.java index f9c7bd9..2e22dfb 100644 --- a/src/main/java/cz/muni/ics/ga4gh/model/Ga4ghClaimRepository.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghClaimRepository.java @@ -1,20 +1,28 @@ -package cz.muni.ics.ga4gh.model; +package cz.muni.ics.ga4gh.base.model; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.ToString; +import org.springframework.validation.annotation.Validated; import org.springframework.web.client.RestTemplate; @Getter @ToString @EqualsAndHashCode @AllArgsConstructor +@Validated public class Ga4ghClaimRepository { + @NotBlank private final String name; + @NotBlank private final String actionURL; + @NotNull private final RestTemplate restTemplate; + } diff --git a/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassport.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassport.java new file mode 100644 index 0000000..5e7a5d9 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassport.java @@ -0,0 +1,34 @@ +package cz.muni.ics.ga4gh.base.model; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import java.util.ArrayList; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +public class Ga4ghPassport { + + private final List<Ga4ghPassportVisa> visas = new ArrayList<>(); + + public void addVisas(List<Ga4ghPassportVisa> visas) { + if (visas == null || visas.isEmpty()) { + return; + } + this.visas.addAll(visas); + } + + public ArrayNode toJsonObject() { + ArrayNode passport = JsonNodeFactory.instance.arrayNode(); + if (!visas.isEmpty()) { + for (Ga4ghPassportVisa visa: visas) { + passport.add(visa.serialize()); + } + } + return passport; + } +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisa.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisa.java new file mode 100644 index 0000000..85381ae --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisa.java @@ -0,0 +1,119 @@ +package cz.muni.ics.ga4gh.base.model; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JOSEObjectType; +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService; +import java.net.URI; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode +public class Ga4ghPassportVisa { + + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + public static final String TYPE_AFFILIATION_AND_ROLE = "AffiliationAndRole"; + public static final String TYPE_ACCEPTED_TERMS_AND_POLICIES = "AcceptedTermsAndPolicies"; + public static final String TYPE_RESEARCHER_STATUS = "ResearcherStatus"; + public static final String TYPE_LINKED_IDENTITIES = "LinkedIdentities"; + public static final String TYPE_CONTROLLED_ACCESS_GRANTS = "ControlledAccessGrants"; + + public static final String BY_SYSTEM = "system"; + public static final String BY_SO = "so"; + public static final String BY_PEER = "peer"; + public static final String BY_SELF = "self"; + + public static final String SUB = "sub"; + public static final String ISS = "iss"; + public static final String IAT = "iat"; + public static final String EXP = "exp"; + public static final String JTI = "jti"; + public static final String TYPE = "type"; + public static final String ASSERTED = "asserted"; + public static final String VALUE = "value"; + public static final String SOURCE = "source"; + public static final String BY = "by"; + public static final String CONDITIONS = "conditions"; + + // === VISA HEADER FIELDS === + private String kid; + + private JOSEObjectType typ; + + private URI jku; + + // === VISA PAYLOAD FIELDS === + + private String iss; + + private String sub; + + private Date iat; + + private Date exp; + + private String jti; + + // value of the visa + private Ga4ghPassportVisaV1 ga4ghVisaV1; + + // === CUSTOM FIELDS FOR WORKING WITH VISA === + + @ToString.Exclude + private String signer = null; + + private boolean verified = false; + + private String linkedIdentity; + private SignedJWT jwt; + + public void generateSignedJwt(JWTSigningAndValidationService jwtService) { + Map<String, Object> passportVisaObject = new HashMap<>(); + passportVisaObject.put(TYPE, ga4ghVisaV1.getType()); + passportVisaObject.put(Ga4ghPassportVisa.ASSERTED, ga4ghVisaV1.getAsserted()); + passportVisaObject.put(Ga4ghPassportVisa.VALUE, ga4ghVisaV1.getValue()); + passportVisaObject.put(Ga4ghPassportVisa.SOURCE, ga4ghVisaV1.getSource()); + passportVisaObject.put(Ga4ghPassportVisa.BY, ga4ghVisaV1.getBy()); + + if (ga4ghVisaV1.getConditions() != null) { + passportVisaObject.put(Ga4ghPassportVisa.CONDITIONS, ga4ghVisaV1.getConditions()); + } + + JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder() + .issuer(iss) + .issueTime(iat) + .expirationTime(exp) + .subject(sub) + .jwtID(jti) + .claim(Ga4ghPassportVisaV1.GA4GH_VISA_V1, passportVisaObject) + .build(); + + JWSHeader + jwsHeader = new JWSHeader.Builder(JWSAlgorithm.parse(jwtService.getSigningAlgorithm().getName())) + .keyID(jwtService.getSignerKeyId()) + .type(JOSEObjectType.JWT) + .jwkURL(jku) + .build(); + + SignedJWT signedVisaJwt = new SignedJWT(jwsHeader, jwtClaimsSet); + jwtService.signJwt(signedVisaJwt); + this.jwt = signedVisaJwt; + } + + public String serialize() { + return jwt.serialize(); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisaV1.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisaV1.java new file mode 100644 index 0000000..082ec4f --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Ga4ghPassportVisaV1.java @@ -0,0 +1,31 @@ +package cz.muni.ics.ga4gh.base.model; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@EqualsAndHashCode +public class Ga4ghPassportVisaV1 { + public static final String GA4GH_VISA_V1 = "ga4gh_visa_v1"; + + // === mandatory === + private long asserted; + + private String source; + + private String type; + + private String value; + + + // === optional === + private String by; + + private JsonNode conditions; + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Group.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Group.java similarity index 75% rename from src/main/java/cz/muni/ics/ga4gh/model/Group.java rename to src/main/java/cz/muni/ics/ga4gh/base/model/Group.java index 49aea56..637fe1b 100644 --- a/src/main/java/cz/muni/ics/ga4gh/model/Group.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Group.java @@ -1,26 +1,31 @@ -package cz.muni.ics.ga4gh.model; +package cz.muni.ics.ga4gh.base.model; import com.fasterxml.jackson.databind.JsonNode; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; - -import java.util.LinkedHashMap; -import java.util.Map; +import org.springframework.validation.annotation.Validated; @Getter @Setter @ToString @EqualsAndHashCode -@NoArgsConstructor +@AllArgsConstructor +@Validated public class Group { + @NotNull private Long id; private Long parentGroupId; + @NotBlank private String name; private String description; @@ -29,6 +34,7 @@ public class Group { private String uuid; + @NotNull private Long voId; private Map<String, JsonNode> attributes = new LinkedHashMap<>(); diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Member.java b/src/main/java/cz/muni/ics/ga4gh/base/model/Member.java similarity index 56% rename from src/main/java/cz/muni/ics/ga4gh/model/Member.java rename to src/main/java/cz/muni/ics/ga4gh/base/model/Member.java index ca73483..5f0b1ce 100644 --- a/src/main/java/cz/muni/ics/ga4gh/model/Member.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/Member.java @@ -1,26 +1,32 @@ -package cz.muni.ics.ga4gh.model; +package cz.muni.ics.ga4gh.base.model; -import cz.muni.ics.ga4gh.enums.MemberStatus; +import cz.muni.ics.ga4gh.base.enums.MemberStatus; +import javax.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; +import org.springframework.validation.annotation.Validated; @Getter @Setter @ToString @EqualsAndHashCode -@NoArgsConstructor @AllArgsConstructor +@Validated public class Member { + @NotNull private Long id; + @NotNull private Long userId; + @NotNull private Long voId; + @NotNull private MemberStatus status; + } diff --git a/src/main/java/cz/muni/ics/ga4gh/model/PerunAttributeValue.java b/src/main/java/cz/muni/ics/ga4gh/base/model/PerunAttributeValue.java similarity index 98% rename from src/main/java/cz/muni/ics/ga4gh/model/PerunAttributeValue.java rename to src/main/java/cz/muni/ics/ga4gh/base/model/PerunAttributeValue.java index 541b63a..1bb71ec 100644 --- a/src/main/java/cz/muni/ics/ga4gh/model/PerunAttributeValue.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/PerunAttributeValue.java @@ -1,4 +1,4 @@ -package cz.muni.ics.ga4gh.model; +package cz.muni.ics.ga4gh.base.model; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -8,7 +8,12 @@ import com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.NumericNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; -import cz.muni.ics.ga4gh.exceptions.InconvertibleValueException; +import cz.muni.ics.ga4gh.base.exceptions.InconvertibleValueException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -16,17 +21,11 @@ import lombok.NoArgsConstructor; import lombok.ToString; import org.springframework.util.StringUtils; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - @Getter +@ToString +@EqualsAndHashCode @NoArgsConstructor @AllArgsConstructor -@EqualsAndHashCode -@ToString public class PerunAttributeValue { public final static String STRING_TYPE = "java.lang.String"; diff --git a/src/main/java/cz/muni/ics/ga4gh/model/UserExtSource.java b/src/main/java/cz/muni/ics/ga4gh/base/model/UserExtSource.java similarity index 77% rename from src/main/java/cz/muni/ics/ga4gh/model/UserExtSource.java rename to src/main/java/cz/muni/ics/ga4gh/base/model/UserExtSource.java index 19fcda5..282bd31 100644 --- a/src/main/java/cz/muni/ics/ga4gh/model/UserExtSource.java +++ b/src/main/java/cz/muni/ics/ga4gh/base/model/UserExtSource.java @@ -1,33 +1,39 @@ -package cz.muni.ics.ga4gh.model; +package cz.muni.ics.ga4gh.base.model; +import java.sql.Timestamp; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; -import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import org.springframework.util.StringUtils; - -import java.sql.Timestamp; +import org.springframework.validation.annotation.Validated; @Getter @Setter -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode @ToString +@EqualsAndHashCode +@AllArgsConstructor +@Validated public class UserExtSource { + @NotNull private Long id; private ExtSource extSource; + @NotBlank private String login; - private int loa = 0; + @Min(0) + private int loa; private boolean persistent; + @NotNull private Timestamp lastAccess; public void setExtSource(ExtSource extSource) { diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/AttributeMappingProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/AttributeMappingProperties.java new file mode 100644 index 0000000..b761ff9 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/AttributeMappingProperties.java @@ -0,0 +1,38 @@ +package cz.muni.ics.ga4gh.base.properties; + +import cz.muni.ics.ga4gh.base.model.AttributeMapping; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.validation.annotation.Validated; + +@Getter +@ToString +@Slf4j + +@Validated +@ConstructorBinding +@ConfigurationProperties(prefix = "attributes") +public class AttributeMappingProperties { + + @NotEmpty + private final Map<String, AttributeMapping> attributeMappings = new HashMap<>(); + + public AttributeMappingProperties(@NotEmpty Map<String, AttributeMapping> attributeMappings) { + + this.attributeMappings.putAll(attributeMappings); + } + + @PostConstruct + public void init() { + log.info("Initialized '{}' properties", this.getClass().getSimpleName()); + log.debug("{}", this); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/BasicAuthProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/BasicAuthProperties.java new file mode 100644 index 0000000..ea5b626 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/BasicAuthProperties.java @@ -0,0 +1,47 @@ +package cz.muni.ics.ga4gh.base.properties; + +import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException; +import cz.muni.ics.ga4gh.base.model.BasicAuthCredentials; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +@Getter +@ToString +@Slf4j + +@Validated +@ConstructorBinding +@ConfigurationProperties(prefix = "basic-auth") +public class BasicAuthProperties { + + @NotEmpty + private final List<BasicAuthCredentials> credentials = new ArrayList<>(); + + public BasicAuthProperties(@NotEmpty List<BasicAuthCredentials> credentials) + throws ConfigurationException + { + for (BasicAuthCredentials c: credentials) { + if (!StringUtils.hasText(c.getUsername()) || !StringUtils.hasText(c.getPassword())) { + throw new ConfigurationException("Invalid basic-auth credentials configured - empty username or password. Check your configuration."); + } + } + + this.credentials.addAll(credentials); + } + + @PostConstruct + public void init() { + log.info("Initialized '{}' properties", this.getClass().getSimpleName()); + log.debug("{}", this); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/BrokerInstanceProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/BrokerInstanceProperties.java new file mode 100644 index 0000000..ab38098 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/BrokerInstanceProperties.java @@ -0,0 +1,89 @@ +package cz.muni.ics.ga4gh.base.properties; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.validation.annotation.Validated; + +@Getter +@ToString +@Slf4j + +@Validated +@ConstructorBinding +public class BrokerInstanceProperties { + + @NotBlank + private final String name; + + @NotBlank + private final String brokerClass; + + @NotBlank + private final String identifierAttribute; + + private final Long membershipVoId; + + private final String bonaFideStatusAttr; + + private final String bonaFideStatusRemsAttr; + + private final String groupAffiliationsAttr; + + private final String affiliationsAttr; + + private final String orgUrlAttr; + + private final Long termsAndPoliciesGroupId; + + private final String source; + + private final List<Ga4ghClaimRepositoryProperties> passportRepositories = new ArrayList<>(); + + private final List<String> whitelistedLinkedIdentitySources = new ArrayList<>(); + + public BrokerInstanceProperties(String name, + String brokerClass, + String identifierAttribute, + Long membershipVoId, + String bonaFideStatusAttr, + String bonaFideStatusRemsAttr, + String groupAffiliationsAttr, + String affiliationsAttr, + String orgUrlAttr, + Long termsAndPoliciesGroupId, + String source, + List<String> whitelistedLinkedIdentitySources, + List<Ga4ghClaimRepositoryProperties> passportRepositories) + { + this.name = name; + this.brokerClass = brokerClass; + this.identifierAttribute = identifierAttribute; + this.membershipVoId = membershipVoId; + this.bonaFideStatusAttr = bonaFideStatusAttr; + this.bonaFideStatusRemsAttr = bonaFideStatusRemsAttr; + this.groupAffiliationsAttr = groupAffiliationsAttr; + this.termsAndPoliciesGroupId = termsAndPoliciesGroupId; + this.affiliationsAttr = affiliationsAttr; + this.orgUrlAttr = orgUrlAttr; + this.source = source; + if (whitelistedLinkedIdentitySources != null) { + this.whitelistedLinkedIdentitySources.addAll(whitelistedLinkedIdentitySources); + } + if (passportRepositories != null) { + this.passportRepositories.addAll(passportRepositories); + } + } + + @PostConstruct + public void init() { + log.info("Initialized '{}' properties", this.getClass().getSimpleName()); + log.debug("{}", this); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghBrokersProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghBrokersProperties.java new file mode 100644 index 0000000..2657f8d --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghBrokersProperties.java @@ -0,0 +1,75 @@ +package cz.muni.ics.ga4gh.base.properties; + +import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.validator.constraints.URL; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.core.io.FileUrlResource; +import org.springframework.validation.annotation.Validated; + +@Getter +@ToString +@Slf4j + +@Validated +@ConstructorBinding +@ConfigurationProperties(prefix = "broker") +public class Ga4ghBrokersProperties { + + @NotEmpty + private final List<String> userIdentificationAttributes = new ArrayList<>(); + + @NotBlank + private final String issuer; + + @NotNull + private final URI jku; + @NotNull + private final FileUrlResource jwkKeystoreFile; + + @NotEmpty + private final List<BrokerInstanceProperties> brokersProperties = new ArrayList<>(); + + public Ga4ghBrokersProperties(@NotEmpty List<String> userIdentificationAttributes, + @NotBlank String issuer, + @URL String jku, + @NotBlank String pathToJwkFile, + @NotEmpty List<BrokerInstanceProperties> brokers) + throws ConfigurationException, MalformedURLException, URISyntaxException + { + try { + jwkKeystoreFile = new FileUrlResource(pathToJwkFile); + if (!this.jwkKeystoreFile.exists()) { + throw new Exception("JWK file does not exist"); + } else if (!this.jwkKeystoreFile.isReadable()) { + throw new Exception("JWK file is not readable"); + } + this.jwkKeystoreFile.getFile(); + } catch (Exception e) { + throw new ConfigurationException("Error when loading JWK keystore file: " + e.getMessage()); + } + this.userIdentificationAttributes.addAll(userIdentificationAttributes); + this.issuer = issuer; + this.jku = new java.net.URL(jku).toURI(); + this.brokersProperties.addAll(brokers); + } + + @PostConstruct + public void init() { + log.info("Initialized '{}' properties", this.getClass().getSimpleName()); + log.debug("{}", this); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghClaimRepositoryProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghClaimRepositoryProperties.java new file mode 100644 index 0000000..ad50ee8 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/Ga4ghClaimRepositoryProperties.java @@ -0,0 +1,32 @@ +package cz.muni.ics.ga4gh.base.properties; + +import cz.muni.ics.ga4gh.base.model.ClaimRepositoryHeader; +import java.util.List; +import javax.validation.constraints.NotBlank; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.validation.annotation.Validated; + +@Getter +@Setter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@Validated +public class Ga4ghClaimRepositoryProperties { + + @NotBlank + private String name; + + @NotBlank + private String url; + + @NotBlank + private String jwks; + + private List<ClaimRepositoryHeader> headers; + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunAdapterProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunAdapterProperties.java new file mode 100644 index 0000000..8bba7f7 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunAdapterProperties.java @@ -0,0 +1,45 @@ +package cz.muni.ics.ga4gh.base.properties; + +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import java.util.Objects; +import javax.annotation.PostConstruct; +import javax.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +@Getter +@ToString +@Slf4j + +@Validated +@ConstructorBinding +@ConfigurationProperties(prefix = "perun.adapter") +public class PerunAdapterProperties { + + @NotBlank + private final String adapterPrimary; + + private final boolean callFallback; + + public PerunAdapterProperties(String primary, Boolean callFallback) { + if (StringUtils.hasText(primary)) { + this.adapterPrimary = primary; + } else { + this.adapterPrimary = PerunAdapter.RPC; + } + + this.callFallback = Objects.requireNonNullElse(callFallback, true); + } + + @PostConstruct + public void init() { + log.info("Initialized '{}' properties", this.getClass().getSimpleName()); + log.debug("{}", this); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunLdapConnectorProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunLdapConnectorProperties.java new file mode 100644 index 0000000..9edb1ae --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunLdapConnectorProperties.java @@ -0,0 +1,86 @@ +package cz.muni.ics.ga4gh.base.properties; + +import java.util.Objects; +import javax.annotation.PostConstruct; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.validation.annotation.Validated; + +@Getter +@Slf4j + +@Validated +@ConstructorBinding +@ConfigurationProperties(prefix = "perun.connector.ldap") +@ConditionalOnProperty("perun.connector.ldap.host") +public class PerunLdapConnectorProperties { + + @NotBlank + private final String host; + + private final String user; + + private final String password; + + @NotBlank + private final String baseDn; + + private final boolean useTls; + + private final boolean useSsl; + + private final boolean allowUntrustedSsl; + + @Min(1) + private final long timeoutSecs; + + private final int port; + + public PerunLdapConnectorProperties(String host, + String user, + String password, + String baseDn, + Boolean useTls, + Boolean useSsl, + Boolean allowUntrustedSsl, + Long timeoutSecs, + Integer port) + { + this.host = host; + this.user = user; + this.password = password; + this.baseDn = baseDn; + this.useTls = Objects.requireNonNullElse(useTls, false); + this.useSsl = Objects.requireNonNullElse(useSsl, false); + this.allowUntrustedSsl = Objects.requireNonNullElse(allowUntrustedSsl, false); + this.timeoutSecs = Objects.requireNonNullElse(timeoutSecs, 5L); + this.port = Objects.requireNonNullElse(port, 336); + } + + @PostConstruct + public void init() { + log.info("Initialized '{}' properties", this.getClass().getSimpleName()); + log.debug("{}", this); + } + + @Override + public String toString() { + return "LdapProperties{" + + "host='" + host + '\'' + + ", user='" + user + '\'' + + ", password='PROTECTED_STRING'" + + ", baseDn='" + baseDn + '\'' + + ", useTls=" + useTls + + ", useSsl=" + useSsl + + ", allowUntrustedSsl=" + allowUntrustedSsl + + ", timeoutSecs=" + timeoutSecs + + ", port=" + port + + '}'; + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunRpcConnectorProperties.java b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunRpcConnectorProperties.java new file mode 100644 index 0000000..ae508cc --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/base/properties/PerunRpcConnectorProperties.java @@ -0,0 +1,79 @@ +package cz.muni.ics.ga4gh.base.properties; + +import java.util.Objects; +import javax.annotation.PostConstruct; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +@Getter +@Slf4j + +@Validated +@ConstructorBinding +@ConfigurationProperties(prefix = "perun.connector.rpc") +public class PerunRpcConnectorProperties { + + @NotBlank + private final String url; + @NotBlank + private final String username; + @NotBlank + private final String password; + private final String serializer; + @Min(1) + private final int connectionTimeout; + @Min(1) + private final int connectionRequestTimeout; + @Min(1) + private final int requestTimeout; + private final boolean enabled; + + public PerunRpcConnectorProperties(String url, + String username, + String password, + String serializer, + Integer connectionTimeout, + Integer connectionRequestTimeout, + Integer requestTimeout, + Boolean enabled) + { + if (!StringUtils.hasText(serializer)) { + serializer = "jsonlite"; + } + this.url = url; + this.username = username; + this.password = password; + this.serializer = serializer; + this.connectionTimeout = Objects.requireNonNullElse(connectionTimeout, 30000); + this.connectionRequestTimeout = Objects.requireNonNullElse(connectionRequestTimeout, 30000); + this.requestTimeout = Objects.requireNonNullElse(requestTimeout, 60000); + this.enabled = Objects.requireNonNullElse(enabled, true); + } + + @PostConstruct + public void init() { + log.info("Initialized '{}' properties", this.getClass().getSimpleName()); + log.debug("{}", this); + } + + @Override + public String toString() { + return "RpcAdapterProperties{" + + "url='" + url + '\'' + + ", username='" + username + '\'' + + ", password='PROTECTED_STRING'" + + ", serializer='" + serializer + '\'' + + ", connectionTimeout='" + connectionTimeout + '\'' + + ", connectionRequestTimeout='" + connectionRequestTimeout + '\'' + + ", requestTimeout='" + requestTimeout + '\'' + + ", enabled='" + enabled + '\'' + + '}'; + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/config/AdapterConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/AdapterConfig.java deleted file mode 100644 index 915582c..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/config/AdapterConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package cz.muni.ics.ga4gh.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties -@ConfigurationProperties(prefix = "adapter") -@Getter -@Setter -public class AdapterConfig { - - private String adapterPrimary; - - private Boolean callFallback; -} diff --git a/src/main/java/cz/muni/ics/ga4gh/config/AttributesConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/AttributesConfig.java deleted file mode 100644 index 469443e..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/config/AttributesConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package cz.muni.ics.ga4gh.config; - -import cz.muni.ics.ga4gh.model.AttributeMapping; -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.util.Map; - -@Configuration -@EnableConfigurationProperties -@ConfigurationProperties(prefix = "attributes") -@Getter -@Setter -public class AttributesConfig { - - private Map<String, AttributeMapping> attributeMappings; -} diff --git a/src/main/java/cz/muni/ics/ga4gh/config/BasicAuthConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/BasicAuthConfig.java deleted file mode 100644 index 5436cdb..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/config/BasicAuthConfig.java +++ /dev/null @@ -1,19 +0,0 @@ -package cz.muni.ics.ga4gh.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties -@ConfigurationProperties(prefix = "basic-auth") -@Getter -@Setter -public class BasicAuthConfig { - - private String username; - - private String password; -} diff --git a/src/main/java/cz/muni/ics/ga4gh/config/BrokerConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/BrokerConfig.java deleted file mode 100644 index ac59aaf..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/config/BrokerConfig.java +++ /dev/null @@ -1,37 +0,0 @@ -package cz.muni.ics.ga4gh.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -@Configuration -@EnableConfigurationProperties -@ConfigurationProperties(prefix = "broker") -@Getter -@Setter -public class BrokerConfig { - - private String bonaFideStatusAttr; - - private String bonaFideStatusRemsAttr; - - private String groupAffiliationsAttr; - - private Long termsAndPoliciesGroupId; - - private String affiliationsAttr; - - private String orgUrlAttr; - - private List<String> attributesToSearch; - - private String issuer; - - private String jku; - - private String pathToJwkFile; -} diff --git a/src/main/java/cz/muni/ics/ga4gh/config/Ga4ghConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/Ga4ghConfig.java deleted file mode 100644 index 188a1de..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/config/Ga4ghConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package cz.muni.ics.ga4gh.config; - -import cz.muni.ics.ga4gh.model.Repo; -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.util.List; - -@Configuration -@EnableConfigurationProperties -@ConfigurationProperties(prefix = "ga4gh") -@Getter -@Setter -public class Ga4ghConfig { - - private List<Repo> repos; -} diff --git a/src/main/java/cz/muni/ics/ga4gh/config/LdapConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/LdapConfig.java deleted file mode 100644 index 8a45101..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/config/LdapConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -package cz.muni.ics.ga4gh.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties -@ConfigurationProperties(prefix = "ldap") -@Getter -@Setter -public class LdapConfig { - - private String host; - - private String user; - - private String password; - - private String baseDn; - - private Boolean useTls; - - private Boolean useSsl; - - private Boolean allowUntrustedSsl; - - private Long timeoutSecs; - - private int port; -} diff --git a/src/main/java/cz/muni/ics/ga4gh/config/RpcConfig.java b/src/main/java/cz/muni/ics/ga4gh/config/RpcConfig.java deleted file mode 100644 index 74f41d8..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/config/RpcConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package cz.muni.ics.ga4gh.config; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties -@ConfigurationProperties(prefix = "rpc") -@Getter -@Setter -public class RpcConfig { - - private Boolean enabled; - - private String url; - - private String username; - - private String password; - - private String serializer; -} diff --git a/src/main/java/cz/muni/ics/ga4gh/controllers/Ga4ghBrokerController.java b/src/main/java/cz/muni/ics/ga4gh/controllers/Ga4ghBrokerController.java deleted file mode 100644 index c37ecf1..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/controllers/Ga4ghBrokerController.java +++ /dev/null @@ -1,35 +0,0 @@ -package cz.muni.ics.ga4gh.controllers; - -import com.fasterxml.jackson.databind.node.ArrayNode; -import cz.muni.ics.ga4gh.facade.Ga4ghBrokerFacade; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -import javax.servlet.http.HttpServletResponse; - -@RestController -@RequestMapping("/ga4gh") -public class Ga4ghBrokerController { - - private final Ga4ghBrokerFacade ga4GhBrokerFacade; - - @Autowired - public Ga4ghBrokerController(Ga4ghBrokerFacade ga4GhBrokerFacade) { - this.ga4GhBrokerFacade = ga4GhBrokerFacade; - } - - @GetMapping(value = "/{eppn}", produces = MediaType.APPLICATION_JSON_VALUE) - public ArrayNode getGa4ghPassport(@PathVariable String eppn, HttpServletResponse response) { - ArrayNode result = ga4GhBrokerFacade.getGa4ghPassport(eppn); - - if (result == null) { - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - } - - return result; - } -} diff --git a/src/main/java/cz/muni/ics/ga4gh/facade/Ga4ghBrokerFacade.java b/src/main/java/cz/muni/ics/ga4gh/facade/Ga4ghBrokerFacade.java index 2c4ebf4..98658ff 100644 --- a/src/main/java/cz/muni/ics/ga4gh/facade/Ga4ghBrokerFacade.java +++ b/src/main/java/cz/muni/ics/ga4gh/facade/Ga4ghBrokerFacade.java @@ -1,8 +1,12 @@ package cz.muni.ics.ga4gh.facade; -import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.JsonNode; +import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException; +import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException; public interface Ga4ghBrokerFacade { - ArrayNode getGa4ghPassport(String eppn); + JsonNode getGa4ghPassport(String userIdentifier) + throws UserNotFoundException, UserNotUniqueException; + } diff --git a/src/main/java/cz/muni/ics/ga4gh/facade/impl/Ga4GhBrokerFacadeImpl.java b/src/main/java/cz/muni/ics/ga4gh/facade/impl/Ga4GhBrokerFacadeImpl.java index bf3ce54..4308aee 100644 --- a/src/main/java/cz/muni/ics/ga4gh/facade/impl/Ga4GhBrokerFacadeImpl.java +++ b/src/main/java/cz/muni/ics/ga4gh/facade/impl/Ga4GhBrokerFacadeImpl.java @@ -1,23 +1,44 @@ package cz.muni.ics.ga4gh.facade.impl; -import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException; +import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassport; import cz.muni.ics.ga4gh.facade.Ga4ghBrokerFacade; import cz.muni.ics.ga4gh.service.Ga4ghBrokerService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; @Service public class Ga4GhBrokerFacadeImpl implements Ga4ghBrokerFacade { - Ga4ghBrokerService ga4GhBrokerService; + private final Ga4ghBrokerService ga4ghBrokerService; @Autowired - public Ga4GhBrokerFacadeImpl(Ga4ghBrokerService ga4GhBrokerService) { - this.ga4GhBrokerService = ga4GhBrokerService; + public Ga4GhBrokerFacadeImpl(Ga4ghBrokerService ga4ghBrokerService) { + this.ga4ghBrokerService = ga4ghBrokerService; } @Override - public ArrayNode getGa4ghPassport(String eppn) { - return ga4GhBrokerService.getGa4ghPassport(eppn); + public JsonNode getGa4ghPassport(String userIdentifier) + throws UserNotFoundException, UserNotUniqueException + { + if (!StringUtils.hasText(userIdentifier)) { + throw new IllegalArgumentException("User identifier cannot be empty"); + } + + Long perunUserId = ga4ghBrokerService.identifyUser(userIdentifier); + if (perunUserId == null) { + throw new UserNotFoundException("No user found for given identifier"); + } + Ga4ghPassport passport = ga4ghBrokerService.getGa4ghPassport(perunUserId); + if (passport == null) { + return JsonNodeFactory.instance.nullNode(); + } else { + return passport.toJsonObject(); + } } + } diff --git a/src/main/java/cz/muni/ics/ga4gh/model/ExtSource.java b/src/main/java/cz/muni/ics/ga4gh/model/ExtSource.java deleted file mode 100644 index eeb1cd9..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/model/ExtSource.java +++ /dev/null @@ -1,40 +0,0 @@ -package cz.muni.ics.ga4gh.model; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; -import org.springframework.util.StringUtils; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@EqualsAndHashCode -@ToString -public class ExtSource { - - @Setter - private Long id; - - private String name; - - private String type; - - public void setName(String name) { - if (StringUtils.hasText(name)) { - throw new IllegalArgumentException("name cannot be null nor empty"); - } - - this.name = name; - } - - public void setType(String type) { - if (!StringUtils.hasText(type)) { - throw new IllegalArgumentException("type cannot be null nor empty"); - } - - this.type = type; - } -} diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Ga4ghPassportVisa.java b/src/main/java/cz/muni/ics/ga4gh/model/Ga4ghPassportVisa.java deleted file mode 100644 index 6917574..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/model/Ga4ghPassportVisa.java +++ /dev/null @@ -1,59 +0,0 @@ -package cz.muni.ics.ga4gh.model; - -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.Setter; -import lombok.ToString; - -@Getter -@Setter -@ToString -@EqualsAndHashCode -public class Ga4ghPassportVisa { - - public static final String GA4GH_VISA_V1 = "ga4gh_visa_v1"; - - public static final String TYPE_AFFILIATION_AND_ROLE = "AffiliationAndRole"; - public static final String TYPE_ACCEPTED_TERMS_AND_POLICIES = "AcceptedTermsAndPolicies"; - public static final String TYPE_RESEARCHER_STATUS = "ResearcherStatus"; - public static final String TYPE_LINKED_IDENTITIES = "LinkedIdentities"; - - public static final String BY_SYSTEM = "system"; - public static final String BY_SO = "so"; - public static final String BY_PEER = "peer"; - public static final String BY_SELF = "self"; - - public static final String SUB = "sub"; - public static final String EXP = "exp"; - public static final String ISS = "iss"; - public static final String TYPE = "type"; - public static final String ASSERTED = "asserted"; - public static final String VALUE = "value"; - public static final String SOURCE = "source"; - public static final String BY = "by"; - public static final String CONDITION = "condition"; - - private boolean verified = false; - private String linkedIdentity; - private String sub; - private String iss; - private String type; - private String value; - - @ToString.Exclude - private String signer; - - @ToString.Exclude - private String jwt; - - @ToString.Exclude - private String prettyPayload; - - public Ga4ghPassportVisa(String jwt) { - this.jwt = jwt; - } - - public String getPrettyString() { - return prettyPayload + ", signed by " + signer; - } -} diff --git a/src/main/java/cz/muni/ics/ga4gh/model/Repo.java b/src/main/java/cz/muni/ics/ga4gh/model/Repo.java deleted file mode 100644 index 23016ac..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/model/Repo.java +++ /dev/null @@ -1,27 +0,0 @@ -package cz.muni.ics.ga4gh.model; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.ToString; - -import java.util.List; - -@Getter -@Setter -@ToString -@EqualsAndHashCode -@NoArgsConstructor -@AllArgsConstructor -public class Repo { - - private String name; - - private String url; - - private String jwks; - - private List<RepoHeader> headers; -} diff --git a/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghBrokerService.java b/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghBrokerService.java index 824d9fa..a3ebd90 100644 --- a/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghBrokerService.java +++ b/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghBrokerService.java @@ -1,8 +1,13 @@ package cz.muni.ics.ga4gh.service; -import com.fasterxml.jackson.databind.node.ArrayNode; +import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException; +import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassport; public interface Ga4ghBrokerService { - ArrayNode getGa4ghPassport(String eppn); + Ga4ghPassport getGa4ghPassport(Long userId) throws UserNotFoundException, UserNotUniqueException; + + Long identifyUser(String userIdentifier) throws UserNotFoundException, UserNotUniqueException; + } diff --git a/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghPassportBrokerBeans.java b/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghPassportBrokerBeans.java new file mode 100644 index 0000000..2843160 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/service/Ga4ghPassportBrokerBeans.java @@ -0,0 +1,56 @@ +package cz.muni.ics.ga4gh.service; + +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import cz.muni.ics.ga4gh.base.exceptions.ConfigurationException; +import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties; +import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties; +import cz.muni.ics.ga4gh.service.impl.brokers.BbmriGa4ghBroker; +import cz.muni.ics.ga4gh.service.impl.brokers.ElixirGa4ghBroker; +import cz.muni.ics.ga4gh.service.impl.brokers.Ga4ghBroker; +import cz.muni.ics.ga4gh.service.impl.brokers.LifescienceRiGa4ghBroker; +import cz.muni.ics.ga4gh.service.impl.brokers.PerunGa4ghBroker; +import java.util.ArrayList; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +@Component +public class Ga4ghPassportBrokerBeans { + + @Autowired + @Bean + public List<Ga4ghBroker> passportBrokers(Ga4ghBrokersProperties ga4ghBrokersProperties, + PerunAdapter perunAdapter, + JWTSigningAndValidationService jwtService) + throws ConfigurationException + { + List<Ga4ghBroker> brokers = new ArrayList<>(); + List<BrokerInstanceProperties> properties = ga4ghBrokersProperties.getBrokersProperties(); + for (BrokerInstanceProperties instanceProperties: properties) { + brokers.add(initializeBroker(instanceProperties, ga4ghBrokersProperties, perunAdapter, jwtService)); + } + return brokers; + } + + private Ga4ghBroker initializeBroker(BrokerInstanceProperties instanceProperties, + Ga4ghBrokersProperties ga4ghBrokersProperties, + PerunAdapter perunAdapter, + JWTSigningAndValidationService jwtService) + throws ConfigurationException + { + String implementationClass = instanceProperties.getBrokerClass(); + switch (implementationClass) { + case "BbmriGa4ghBroker": + return new BbmriGa4ghBroker(instanceProperties, ga4ghBrokersProperties, perunAdapter, jwtService); + case "ElixirGa4ghBroker": + return new ElixirGa4ghBroker(instanceProperties, ga4ghBrokersProperties, perunAdapter, jwtService); + case "LifescienceRiGa4ghBroker": + return new LifescienceRiGa4ghBroker(instanceProperties, ga4ghBrokersProperties, perunAdapter, jwtService); + case "PerunGa4ghBroker": + return new PerunGa4ghBroker(instanceProperties, ga4ghBrokersProperties, perunAdapter, jwtService); + default: + throw new ConfigurationException("Invalid broker class name specified"); + } + } +} diff --git a/src/main/java/cz/muni/ics/ga4gh/service/JWTSigningAndValidationService.java b/src/main/java/cz/muni/ics/ga4gh/service/JWTSigningAndValidationService.java index 9748662..16d84e6 100644 --- a/src/main/java/cz/muni/ics/ga4gh/service/JWTSigningAndValidationService.java +++ b/src/main/java/cz/muni/ics/ga4gh/service/JWTSigningAndValidationService.java @@ -3,17 +3,16 @@ package cz.muni.ics.ga4gh.service; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jwt.SignedJWT; - -import java.util.Collection; import java.util.Map; public interface JWTSigningAndValidationService { - Map<String, JWK> getAllPublicKeys(); - JWSAlgorithm getDefaultSigningAlgorithm(); + JWSAlgorithm getSigningAlgorithm(); + + String getSignerKeyId(); - Collection<JWSAlgorithm> getAllSigningAlgsSupported(); + Map<String, JWK> getPublicKeys(); /** * Called to sign a jwt in place for a client that hasn't registered a preferred signing algorithm. @@ -24,14 +23,4 @@ public interface JWTSigningAndValidationService { */ void signJwt(SignedJWT jwt); - /** - * Sign a jwt using the selected algorithm. The algorithm is selected using the String parameter values specified - * in the JWT spec, section 6. I.E., "HS256" means HMAC with SHA-256 and corresponds to our HmacSigner class. - * - * @param jwt the jwt to sign - * @param alg the name of the algorithm to use, as specified in JWS s.6 - */ - void signJwt(SignedJWT jwt, JWSAlgorithm alg); - - String getDefaultSignerKeyId(); } diff --git a/src/main/java/cz/muni/ics/ga4gh/service/PassportAssemblyContext.java b/src/main/java/cz/muni/ics/ga4gh/service/PassportAssemblyContext.java new file mode 100644 index 0000000..808cbfe --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/service/PassportAssemblyContext.java @@ -0,0 +1,35 @@ +package cz.muni.ics.ga4gh.service; + +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import cz.muni.ics.ga4gh.base.model.Affiliation; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa; +import java.util.List; +import java.util.Set; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@Builder +public class PassportAssemblyContext { + + private long now; + + private PerunAdapter perunAdapter; + + private String subject; + + private long perunUserId; + + private List<Affiliation> identityAffiliations; + + private Set<String> externalLinkedIdentities; + + private List<Ga4ghPassportVisa> externalControlledAccessGrants; + + private List<Ga4ghPassportVisa> resultVisas; + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/Ga4GhBrokerBrokerServiceImpl.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/Ga4GhBrokerBrokerServiceImpl.java index 9999334..8dc05ad 100644 --- a/src/main/java/cz/muni/ics/ga4gh/service/impl/Ga4GhBrokerBrokerServiceImpl.java +++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/Ga4GhBrokerBrokerServiceImpl.java @@ -1,61 +1,91 @@ package cz.muni.ics.ga4gh.service.impl; -import com.fasterxml.jackson.databind.node.ArrayNode; -import cz.muni.ics.ga4gh.adapters.PerunAdapter; -import cz.muni.ics.ga4gh.config.AttributesConfig; -import cz.muni.ics.ga4gh.config.BrokerConfig; -import cz.muni.ics.ga4gh.exceptions.UserNotUniqueException; -import cz.muni.ics.ga4gh.model.AttributeMapping; -import cz.muni.ics.ga4gh.service.impl.brokers.Ga4ghBroker; +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException; +import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassport; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa; +import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties; import cz.muni.ics.ga4gh.service.Ga4ghBrokerService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - +import cz.muni.ics.ga4gh.service.impl.brokers.Ga4ghBroker; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; @Service @Slf4j public class Ga4GhBrokerBrokerServiceImpl implements Ga4ghBrokerService { - private final Ga4ghBroker broker; + private final List<Ga4ghBroker> brokers = new ArrayList<>(); private final PerunAdapter adapter; - private final List<String> attributesToSearch; - - private final Map<String, AttributeMapping> attributes; + private final List<String> userIdentificationAttributes = new ArrayList<>(); @Autowired - public Ga4GhBrokerBrokerServiceImpl(Ga4ghBroker broker, PerunAdapter adapter, BrokerConfig brokerConfig, AttributesConfig attributesConfig) { - this.broker = broker; + public Ga4GhBrokerBrokerServiceImpl(List<Ga4ghBroker> brokers, + PerunAdapter adapter, + Ga4ghBrokersProperties brokersProperties) + { + this.brokers.addAll(brokers); this.adapter = adapter; - this.attributesToSearch = brokerConfig.getAttributesToSearch(); - this.attributes = attributesConfig.getAttributeMappings(); + this.userIdentificationAttributes.addAll(brokersProperties.getUserIdentificationAttributes()); } @Override - public ArrayNode getGa4ghPassport(String eppn) { - Set<Long> userIds = new HashSet<>(); - - for (String attrName : attributesToSearch) { - if (attributes.get(attrName) != null) { - userIds.addAll(adapter.getAdapterPrimary().getUserIdsByAttributeValue(attributes.get(attrName), eppn)); + public Ga4ghPassport getGa4ghPassport(Long userId) { + Ga4ghPassport passport = new Ga4ghPassport(); + long startTime = System.currentTimeMillis(); + for (Ga4ghBroker broker: brokers) { + long localStartTime = System.currentTimeMillis(); + List<Ga4ghPassportVisa> visas = broker.constructGa4ghPassportVisas(userId); + if (visas != null) { + passport.addVisas(visas); } + long localEndTime = System.currentTimeMillis(); + log.info("Generating for user '{}' in broker '{}({})' took {}ms", userId, broker.getBrokerName(), broker.getClass().getSimpleName(), localEndTime - localStartTime); } + long endTime = System.currentTimeMillis(); + log.info("Generating for user '{}' in total took {}ms", userId, endTime - startTime); + return passport; + } - if (userIds.isEmpty()) { - log.debug("User {} not found", eppn); - return null; + @Override + public Long identifyUser(String userIdentifier) + throws UserNotFoundException, UserNotUniqueException + { + if (!StringUtils.hasText(userIdentifier)) { + throw new IllegalArgumentException("User identifier cannot be empty"); } - if (userIds.size() > 1) { - throw new UserNotUniqueException("There are more users found by " + eppn + " - " + String.join(", ", userIds.toString())); + Set<Long> perunUserIds = new HashSet<>(); + + for (String attrName : userIdentificationAttributes) { + Set<Long> foundUserIds = adapter.getUserIdsByAttributeValue(attrName, userIdentifier); + if (foundUserIds == null || foundUserIds.isEmpty()) { + log.debug("No user IDs found for identifier '{}' and attribute '{}'", + userIdentifier, attrName); + continue; + } + perunUserIds.addAll(foundUserIds); } - return broker.constructGa4ghPassportVisa(userIds.iterator().next(), eppn); + if (perunUserIds.isEmpty()) { + log.debug("User with identifier '{}' not found", userIdentifier); + throw new UserNotFoundException("User with identifier '" + userIdentifier + "' not found"); + } + + if (perunUserIds.size() > 1) { + log.warn("Multiple users found with identifier '{}' - user IDS: '{}'", userIdentifier, perunUserIds); + throw new UserNotUniqueException("More than one user found for identifier '" + + userIdentifier + '\''); + } + return perunUserIds.iterator().next(); } + } diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/JWTSigningAndValidationServiceImpl.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/JWTSigningAndValidationServiceImpl.java index 0fc7b3b..4865df2 100644 --- a/src/main/java/cz/muni/ics/ga4gh/service/impl/JWTSigningAndValidationServiceImpl.java +++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/JWTSigningAndValidationServiceImpl.java @@ -2,7 +2,6 @@ package cz.muni.ics.ga4gh.service.impl; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; -import com.nimbusds.jose.JWSProvider; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.crypto.ECDSASigner; @@ -16,20 +15,16 @@ import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.OctetSequenceKey; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jwt.SignedJWT; -import cz.muni.ics.ga4gh.jose.keystore.JWKSetKeyStore; +import cz.muni.ics.ga4gh.base.JWKSetKeyStore; import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - @Slf4j @Service public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidationService { @@ -37,20 +32,9 @@ public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidati private final Map<String, JWSSigner> signers = new HashMap<>(); private final Map<String, JWSVerifier> verifiers = new HashMap<>(); - private String defaultSignerKeyId; - private JWSAlgorithm defaultAlgorithm; - private Map<String, JWK> keys = new HashMap<>(); - - /** - * Build this service based on the keys given. All public keys will be used - * to make verifiers, all private keys will be used to make signers. - * - * @param keys A map of key identifier to key. - */ - public JWTSigningAndValidationServiceImpl(Map<String, JWK> keys) { - this.keys = keys; - buildSignersAndVerifiers(); - } + private String signerKeyId; + private JWSAlgorithm signingAlgorithm; + private final Map<String, JWK> keys = new HashMap<>(); /** * Build this service based on the given keystore. All keys must have a key @@ -59,57 +43,47 @@ public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidati * @param keyStore The keystore to load all keys from. */ @Autowired - public JWTSigningAndValidationServiceImpl(JWKSetKeyStore keyStore) { - if (keyStore!= null && keyStore.getJwkSet() != null) { - for (JWK key : keyStore.getKeys()) { - if (!StringUtils.isEmpty(key.getKeyID())) { - this.keys.put(key.getKeyID(), key); - } else { - String fakeKid = UUID.randomUUID().toString(); - this.keys.put(fakeKid, key); - } - } + public JWTSigningAndValidationServiceImpl(JWKSetKeyStore keyStore) throws Exception { + loadKeysFromStore(keyStore); + initializeSignersAndVerifiers(); + + if (keyStore != null) { + JWK defaultKey = keyStore.getKeys().get(0); + setSignerKeyId(defaultKey.getKeyID()); + setDefaultSigningAlgorithmName(defaultKey.getAlgorithm().getName()); + } else { + throw new Exception("Failed to initialize keystore"); } - - buildSignersAndVerifiers(); - - this.defaultSignerKeyId = keyStore.getKeys().get(0).getKeyID(); - setDefaultSigningAlgorithmName(keyStore.getKeys().get(0).getAlgorithm().getName()); } @Override - public String getDefaultSignerKeyId() { - return defaultSignerKeyId; + public String getSignerKeyId() { + return signerKeyId; } - public void setDefaultSignerKeyId(String defaultSignerId) { - this.defaultSignerKeyId = defaultSignerId; + public void setSignerKeyId(String defaultSignerId) { + this.signerKeyId = defaultSignerId; } @Override - public JWSAlgorithm getDefaultSigningAlgorithm() { - return defaultAlgorithm; + public JWSAlgorithm getSigningAlgorithm() { + return signingAlgorithm; } public void setDefaultSigningAlgorithmName(String algName) { - defaultAlgorithm = JWSAlgorithm.parse(algName); - } - - public String getDefaultSigningAlgorithmName() { - if (defaultAlgorithm != null) { - return defaultAlgorithm.getName(); - } else { - return null; - } + signingAlgorithm = JWSAlgorithm.parse(algName); } @Override public void signJwt(SignedJWT jwt) { - if (getDefaultSignerKeyId() == null) { - throw new IllegalStateException("Tried to call default signing with no default signer ID set"); + if (getSignerKeyId() == null) { + throw new IllegalStateException("No signer key ID is set"); } - JWSSigner signer = signers.get(getDefaultSignerKeyId()); + JWSSigner signer = signers.getOrDefault(getSignerKeyId(), null); + if (signer == null) { + throw new IllegalStateException("No signer found for set signer key ID"); + } try { jwt.sign(signer); @@ -119,29 +93,7 @@ public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidati } @Override - public void signJwt(SignedJWT jwt, JWSAlgorithm alg) { - JWSSigner signer = null; - - for (JWSSigner s : signers.values()) { - if (s.supportedJWSAlgorithms().contains(alg)) { - signer = s; - break; - } - } - - if (signer == null) { - log.error("No matching algorithm found for alg={}", alg); - } else { - try { - jwt.sign(signer); - } catch (JOSEException e) { - log.error("Failed to sign JWT, error was: ", e); - } - } - } - - @Override - public Map<String, JWK> getAllPublicKeys() { + public Map<String, JWK> getPublicKeys() { Map<String, JWK> pubKeys = new HashMap<>(); keys.keySet().forEach(keyId -> { @@ -155,16 +107,20 @@ public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidati return pubKeys; } - @Override - public Collection<JWSAlgorithm> getAllSigningAlgsSupported() { - Set<JWSAlgorithm> algs = new HashSet<>(); - signers.values().stream().map(JWSProvider::supportedJWSAlgorithms).forEach(algs::addAll); - verifiers.values().stream().map(JWSProvider::supportedJWSAlgorithms).forEach(algs::addAll); - - return algs; + private void loadKeysFromStore(JWKSetKeyStore keyStore) { + if (keyStore != null && keyStore.getJwkSet() != null) { + for (JWK key : keyStore.getKeys()) { + if (StringUtils.hasText(key.getKeyID())) { + this.keys.put(key.getKeyID(), key); + } else { + String fakeKid = UUID.randomUUID().toString(); + this.keys.put(fakeKid, key); + } + } + } } - private void buildSignersAndVerifiers() { + private void initializeSignersAndVerifiers() { for (Map.Entry<String, JWK> jwkEntry : keys.entrySet()) { String id = jwkEntry.getKey(); JWK jwk = jwkEntry.getValue(); @@ -184,8 +140,8 @@ public class JWTSigningAndValidationServiceImpl implements JWTSigningAndValidati } } - if (defaultSignerKeyId == null && keys.size() == 1) { - setDefaultSignerKeyId(keys.keySet().iterator().next()); + if (signerKeyId == null && keys.size() == 1) { + setSignerKeyId(keys.keySet().iterator().next()); } } diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/VisaAssemblyParameters.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/VisaAssemblyParameters.java new file mode 100644 index 0000000..9c1a875 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/VisaAssemblyParameters.java @@ -0,0 +1,37 @@ +package cz.muni.ics.ga4gh.service.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService; +import java.net.URI; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@Builder +@ToString +@EqualsAndHashCode +@NoArgsConstructor +@AllArgsConstructor +public class VisaAssemblyParameters { + + private String issuer; + private URI jku; + private String type; + private String sub; + private Long userId; + private String value; + private String source; + private String by; + private long asserted; + private long expires; + private JsonNode conditions; + private String signer; + private JWTSigningAndValidationService jwtService; + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/BbmriGa4ghBroker.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/BbmriGa4ghBroker.java index f9a89b7..139752d 100644 --- a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/BbmriGa4ghBroker.java +++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/BbmriGa4ghBroker.java @@ -1,190 +1,285 @@ package cz.muni.ics.ga4gh.service.impl.brokers; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import cz.muni.ics.ga4gh.adapters.PerunAdapter; -import cz.muni.ics.ga4gh.config.BrokerConfig; -import cz.muni.ics.ga4gh.config.Ga4ghConfig; -import cz.muni.ics.ga4gh.model.Affiliation; -import cz.muni.ics.ga4gh.model.Ga4ghClaimRepository; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_PEER; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SELF; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SO; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SYSTEM; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_CONTROLLED_ACCESS_GRANTS; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS; + +import cz.muni.ics.ga4gh.base.Utils; +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import cz.muni.ics.ga4gh.base.model.Affiliation; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa; +import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties; +import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties; import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService; -import cz.muni.ics.ga4gh.utils.Utils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -import java.net.MalformedURLException; -import java.net.URISyntaxException; +import cz.muni.ics.ga4gh.service.PassportAssemblyContext; +import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters; import java.sql.Timestamp; -import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_PEER; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SELF; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SO; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SYSTEM; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS; - -@Service -@Profile("bbmri") +@Slf4j public class BbmriGa4ghBroker extends Ga4ghBroker { private static final String BONA_FIDE_URL = "https://doi.org/10.1038/s41431-018-0219-y"; private static final String BBMRI_ERIC_ORG_URL = "https://www.bbmri-eric.eu/"; - private static final String BBMRI_ID = "bbmri_id"; private static final String FACULTY_AT = "faculty@"; + private final String getBbmriIdAttribute; private final String bonaFideStatusAttr; private final String groupAffiliationsAttr; private final Long termsAndPoliciesGroupId; - @Autowired - public BbmriGa4ghBroker(BrokerConfig brokerConfig, PerunAdapter adapter, JWTSigningAndValidationService jwtService, Ga4ghConfig ga4ghConfig) throws URISyntaxException, MalformedURLException { - super(adapter, jwtService, ga4ghConfig, brokerConfig); + private final Long bbmriVoId; + + public BbmriGa4ghBroker(BrokerInstanceProperties instanceProperties, + Ga4ghBrokersProperties brokersProperties, + PerunAdapter adapter, + JWTSigningAndValidationService jwtService) + { + super(instanceProperties, brokersProperties, adapter, jwtService); + + this.getBbmriIdAttribute = instanceProperties.getIdentifierAttribute(); + this.bonaFideStatusAttr = instanceProperties.getBonaFideStatusAttr(); + this.groupAffiliationsAttr = instanceProperties.getGroupAffiliationsAttr(); + this.termsAndPoliciesGroupId = instanceProperties.getTermsAndPoliciesGroupId(); + this.bbmriVoId = instanceProperties.getMembershipVoId(); + } + + @Override + protected String getSubAttribute() { + return getBbmriIdAttribute; + } - bonaFideStatusAttr = brokerConfig.getBonaFideStatusAttr(); - groupAffiliationsAttr = brokerConfig.getGroupAffiliationsAttr(); - termsAndPoliciesGroupId = Objects.requireNonNullElse(brokerConfig.getTermsAndPoliciesGroupId(), 10432L); + @Override + protected Long getCommunityVoId() { + return bbmriVoId; } @Override - protected void addAffiliationAndRoles(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId) + protected void addAffiliationAndRoles(PassportAssemblyContext ctx) { - //by=system for users with affiliation asserted by their IdP (set in UserExtSource attribute "affiliation") - if (affiliations == null) { + String type = TYPE_AFFILIATION_AND_ROLE; + logAddingVisas(type); + if (!isCommunityMember(ctx.getPerunUserId())) { + log.debug("User is not member of the BBMRI community, not adding any {} visas", type); return; } - - for (Affiliation affiliation: affiliations) { - //expires 1 year after the last login from the IdP asserting the affiliation - long expires = Utils.getOneYearExpires(affiliation.getAsserted()); - if (expires < now) { - continue; - } - - JsonNode visa = createPassportVisa(TYPE_AFFILIATION_AND_ROLE, sub, userId, affiliation.getValue(), affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null); - if (visa != null) { - passport.add(visa); - } + Affiliation affiliate = new Affiliation( + null, "affiliate@bbmri.eu",System.currentTimeMillis() / 1000L); + String value = affiliate.getValue(); + Ga4ghPassportVisa affiliateVisa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(affiliate.getSource()) + .by(BY_SYSTEM) + .asserted(affiliate.getAsserted()) + .expires(Utils.getOneYearExpires(affiliate.getAsserted())) + .conditions(null) + .build() + ); + + if (affiliateVisa != null) { + ctx.getResultVisas().add(affiliateVisa); + logAddedVisa(type, value); } } @Override - protected void addAcceptedTermsAndPolicies(long now, ArrayNode passport, Long userId, String sub) { - //by=self for members of the group 10432 "Bona Fide Researchers" - boolean userInGroup = adapter.isUserInGroup(userId, termsAndPoliciesGroupId); + protected void addAcceptedTermsAndPolicies(PassportAssemblyContext ctx) { + String type = TYPE_ACCEPTED_TERMS_AND_POLICIES; + logAddingVisas(type); + if (termsAndPoliciesGroupId == null) { + log.debug("Group ID for accepted terms and policies is not defined, not adding any {} visas", type); + return; + } + + boolean userInGroup = adapter.isUserInGroup(ctx.getPerunUserId(), termsAndPoliciesGroupId); if (!userInGroup) { + log.debug("User is not in the group representing terms and policies approval, not adding any {} visas", type); return; } - long asserted = now; - if (bonaFideStatusAttr != null) { - String bonaFideStatusCreatedAt = adapter.getAdapterRpc().getUserAttributeCreatedAt(userId, bonaFideStatusAttr); - if (bonaFideStatusCreatedAt != null) { - asserted = Timestamp.valueOf(bonaFideStatusCreatedAt).getTime() / 1000L; - } + long asserted = ctx.getNow(); + String bonaFideStatusCreatedAt = adapter.getAdapterRpc() + .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusAttr); + if (bonaFideStatusCreatedAt != null) { + asserted = Timestamp.valueOf(bonaFideStatusCreatedAt).getTime() / 1000L; } long expires = Utils.getExpires(asserted, 100L); - if (expires < now) { - return; - } - JsonNode visa = createPassportVisa(TYPE_ACCEPTED_TERMS_AND_POLICIES, sub, userId, BONA_FIDE_URL, BBMRI_ERIC_ORG_URL, BY_SELF, asserted, expires, null); + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(BBMRI_ERIC_ORG_URL) + .by(BY_SELF) + .asserted(asserted) + .expires(expires) + .conditions(null) + .build() + ); + if (visa != null) { - passport.add(visa); + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); } } @Override - protected void addResearcherStatuses(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId) - { - addResearcherStatusFromBonaFideAttribute(now, passport, userId, sub); - addResearcherStatusFromAffiliation(affiliations, now, passport, sub, userId); - addResearcherStatusGroupAffiliations(now, passport, sub, userId); - } - - @Override - protected void addControlledAccessGrants(long now, ArrayNode passport, String sub, Long userId) { - if (claimRepositories.isEmpty()) { + protected void addControlledAccessGrants(PassportAssemblyContext ctx) { + String type = TYPE_CONTROLLED_ACCESS_GRANTS; + logAddingVisas(type); + List<Ga4ghPassportVisa> controlledAccessGrants = ctx.getExternalControlledAccessGrants(); + if (controlledAccessGrants == null || controlledAccessGrants.isEmpty()) { + log.debug("No external {} visas available, not adding any {} visas", type, type); return; } + ctx.getResultVisas().addAll(controlledAccessGrants); + } - Set<String> linkedIdentities = new HashSet<>(); - for (Ga4ghClaimRepository repo: claimRepositories) { - callPermissionsJwtAPI(repo, Collections.singletonMap(BBMRI_ID, sub), passport, linkedIdentities); - } - - if (linkedIdentities.isEmpty()) { + @Override + protected void addLinkedIdentities(PassportAssemblyContext ctx) { + String type = TYPE_LINKED_IDENTITIES; + logAddingVisas(type); + Set<String> externalLinkedIdentities = ctx.getExternalLinkedIdentities(); + if (externalLinkedIdentities == null || externalLinkedIdentities.isEmpty()) { + log.debug("No external {} visas available, not adding any {} visas", type, type); return; } - - for (String linkedIdentity : linkedIdentities) { - long expires = Utils.getOneYearExpires(now); - - JsonNode visa = createPassportVisa(TYPE_LINKED_IDENTITIES, sub, userId, linkedIdentity, BBMRI_ERIC_ORG_URL, BY_SYSTEM, now, expires, null); + for (String identity: externalLinkedIdentities) { + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(identity) + .source(BBMRI_ERIC_ORG_URL) + .by(BY_SYSTEM) + .asserted(ctx.getNow()) + .expires(Utils.getOneYearExpires(ctx.getNow())) + .conditions(null) + .build() + ); if (visa != null) { - passport.add(visa); + ctx.getResultVisas().add(visa); + logAddedVisa(type, identity); } } } - private void addResearcherStatusFromBonaFideAttribute(long now, ArrayNode passport, Long userId, String sub) + @Override + protected void addResearcherStatuses(PassportAssemblyContext ctx) { - //by=peer for users with attribute elixirBonaFideStatusREMS - String bbmriBonaFideStatusCreatedAt = adapter.getAdapterRpc().getUserAttributeCreatedAt(userId, bonaFideStatusAttr); + logAddingVisas(TYPE_RESEARCHER_STATUS); + // NOT YET DEFINED + /* Below is the approach from LS AAI and ELIXIR AAI + addResearcherStatusFromBonaFideAttribute(ctx); + addResearcherStatusFromAffiliation(ctx); + addResearcherStatusGroupAffiliations(ctx); + */ + } + + private void addResearcherStatusFromBonaFideAttribute(PassportAssemblyContext ctx) + { + String type = TYPE_RESEARCHER_STATUS; + log.debug("Adding {} visa (from bona fide status)", type); + if (!StringUtils.hasText(bonaFideStatusAttr)) { + log.debug("BonaFideStatus attribute is not defined, not adding any {} visas (from bona fide status)", type); + return; + } + String bbmriBonaFideStatusCreatedAt = adapter.getAdapterRpc() + .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusAttr); if (bbmriBonaFideStatusCreatedAt == null) { + log.debug("BBMRI broker - rems bona fide status attr not defined, skipping visa"); return; } long asserted = Timestamp.valueOf(bbmriBonaFideStatusCreatedAt).getTime() / 1000L; long expires = Utils.getOneYearExpires(asserted); - if (expires > now) { - JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, BBMRI_ERIC_ORG_URL, BY_PEER, asserted, expires, null); + String value = BONA_FIDE_URL; + if (expires > ctx.getNow()) { + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(BBMRI_ERIC_ORG_URL) + .by(BY_PEER) + .asserted(asserted) + .expires(expires) + .conditions(null) + .build() + ); if (visa != null) { - passport.add(visa); + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); } } } - private void addResearcherStatusFromAffiliation(List<Affiliation> affiliations, long now, ArrayNode passport, String sub, Long userId) - { - //by=system for users with faculty affiliation asserted by their IdP (set in UserExtSource attribute "affiliation") - if (affiliations == null) { + private void addResearcherStatusFromAffiliation(PassportAssemblyContext ctx) { + String type = TYPE_RESEARCHER_STATUS; + log.debug("Adding {} visa (from affiliations)", type); + if (ctx.getIdentityAffiliations() == null || ctx.getIdentityAffiliations().isEmpty()) { + log.debug("No affiliations available, not adding any {} visas (from affiliations)", type); return; } - for (Affiliation affiliation: affiliations) { + for (Affiliation affiliation: ctx.getIdentityAffiliations()) { if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) { continue; } - long expires = Utils.getOneYearExpires(affiliation.getAsserted()); - if (expires < now) { - continue; - } + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(affiliation.getSource()) + .by(BY_SYSTEM) + .asserted(affiliation.getAsserted()) + .expires(Utils.getOneYearExpires(affiliation.getAsserted())) + .conditions(null) + .build() + ); - JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null); if (visa != null) { - passport.add(visa); + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); } } } - private void addResearcherStatusGroupAffiliations(long now, ArrayNode passport, String sub, Long userId) { - //by=so for users with faculty affiliation asserted by membership in a group with groupAffiliations attribute - List<Affiliation> groupAffiliations = adapter.getGroupAffiliations(userId, groupAffiliationsAttr); - if (groupAffiliations == null) { + private void addResearcherStatusGroupAffiliations(PassportAssemblyContext ctx) { + String type = TYPE_RESEARCHER_STATUS; + log.debug("Adding {} visa (from group affiliations)", type); + if (!StringUtils.hasText(groupAffiliationsAttr)) { + log.debug("GroupAffiliations attribute is not defined, not adding any {} visas (from group affiliations)", type); + return; + } + List<Affiliation> groupAffiliations = adapter.getGroupAffiliations(ctx.getPerunUserId(), bbmriVoId, groupAffiliationsAttr); + if (groupAffiliations == null || groupAffiliations.isEmpty()) { return; } @@ -193,12 +288,26 @@ public class BbmriGa4ghBroker extends Ga4ghBroker { continue; } - long expires = Utils.getOneYearExpires(now); + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(BBMRI_ERIC_ORG_URL) + .by(BY_SO) + .asserted(affiliation.getAsserted()) + .expires(Utils.getOneYearExpires(affiliation.getAsserted())) + .conditions(null) + .build() + ); - JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, BBMRI_ERIC_ORG_URL, BY_SO, affiliation.getAsserted(), expires, null); if (visa != null) { - passport.add(visa); + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); } } } + } diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/ElixirGa4ghBroker.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/ElixirGa4ghBroker.java index cdef905..8e7ef77 100644 --- a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/ElixirGa4ghBroker.java +++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/ElixirGa4ghBroker.java @@ -1,144 +1,209 @@ package cz.muni.ics.ga4gh.service.impl.brokers; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import cz.muni.ics.ga4gh.adapters.PerunAdapter; -import cz.muni.ics.ga4gh.config.BrokerConfig; -import cz.muni.ics.ga4gh.config.Ga4ghConfig; -import cz.muni.ics.ga4gh.model.Affiliation; -import cz.muni.ics.ga4gh.model.Ga4ghClaimRepository; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_PEER; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SELF; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SO; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SYSTEM; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_CONTROLLED_ACCESS_GRANTS; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS; + +import cz.muni.ics.ga4gh.base.Utils; +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import cz.muni.ics.ga4gh.base.model.Affiliation; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa; +import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties; +import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties; import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService; -import cz.muni.ics.ga4gh.utils.Utils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; - -import java.net.MalformedURLException; -import java.net.URISyntaxException; +import cz.muni.ics.ga4gh.service.PassportAssemblyContext; +import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters; import java.sql.Timestamp; -import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Objects; import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_PEER; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SELF; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SO; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.BY_SYSTEM; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES; -import static cz.muni.ics.ga4gh.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS; - -@Service -@Profile("elixir") +@Slf4j public class ElixirGa4ghBroker extends Ga4ghBroker { private static final String BONA_FIDE_URL = "https://doi.org/10.1038/s41431-018-0219-y"; private static final String ELIXIR_ORG_URL = "https://elixir-europe.org/"; - private static final String ELIXIR_ID = "elixir_id"; private static final String FACULTY_AT = "faculty@"; + private final String elixirIdAttribute; private final String bonaFideStatusAttr; private final String bonaFideStatusREMSAttr; private final String groupAffiliationsAttr; private final Long termsAndPoliciesGroupId; - @Autowired - public ElixirGa4ghBroker(BrokerConfig brokerConfig, Ga4ghConfig ga4ghConfig, PerunAdapter adapter, JWTSigningAndValidationService jwtService) throws URISyntaxException, MalformedURLException { - super(adapter, jwtService, ga4ghConfig, brokerConfig); + private final Long elixirVoId; - bonaFideStatusAttr = brokerConfig.getBonaFideStatusAttr(); - bonaFideStatusREMSAttr = brokerConfig.getBonaFideStatusRemsAttr(); - groupAffiliationsAttr = brokerConfig.getGroupAffiliationsAttr(); - termsAndPoliciesGroupId = Objects.requireNonNullElse(brokerConfig.getTermsAndPoliciesGroupId(), 10432L); + public ElixirGa4ghBroker(BrokerInstanceProperties instanceProperties, + Ga4ghBrokersProperties brokersProperties, + PerunAdapter adapter, + JWTSigningAndValidationService jwtService) + { + super(instanceProperties, brokersProperties, adapter, jwtService); + + this.elixirIdAttribute = instanceProperties.getIdentifierAttribute(); + this.bonaFideStatusAttr = instanceProperties.getBonaFideStatusAttr(); + this.bonaFideStatusREMSAttr = instanceProperties.getBonaFideStatusRemsAttr(); + this.groupAffiliationsAttr = instanceProperties.getGroupAffiliationsAttr(); + this.termsAndPoliciesGroupId = instanceProperties.getTermsAndPoliciesGroupId(); + this.elixirVoId = instanceProperties.getMembershipVoId(); } @Override - protected void addAffiliationAndRoles(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId) { - if (affiliations == null) { - return; - } - - for (Affiliation affiliation: affiliations) { - long expires = Utils.getOneYearExpires(affiliation.getAsserted()); - - if (expires < now) { - continue; - } + protected String getSubAttribute() { + return elixirIdAttribute; + } - JsonNode visa = createPassportVisa(TYPE_AFFILIATION_AND_ROLE, sub, userId, affiliation.getValue(), - affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null); + @Override + protected Long getCommunityVoId() { + return elixirVoId; + } - if (visa != null) { - passport.add(visa); - } + @Override + protected void addAffiliationAndRoles(PassportAssemblyContext ctx) + { + String type = TYPE_AFFILIATION_AND_ROLE; + logAddingVisas(type); + if (!isCommunityMember(ctx.getPerunUserId())) { + log.debug("User is not member of the ELIXIR community, not adding any {} visas", type); + return; + } + Affiliation affiliate = new Affiliation(null, + "affiliate@elixir-europe.org", System.currentTimeMillis() / 1000L); + Ga4ghPassportVisa affiliateVisa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(affiliate.getValue()) + .source(affiliate.getSource()) + .by(BY_SYSTEM) + .asserted(affiliate.getAsserted()) + .expires(Utils.getOneYearExpires(affiliate.getAsserted())) + .conditions(null) + .build() + ); + + if (affiliateVisa != null) { + ctx.getResultVisas().add(affiliateVisa); + logAddedVisa(type, affiliate.getValue()); } } @Override - protected void addAcceptedTermsAndPolicies(long now, ArrayNode passport, Long userId, String sub) { - boolean userInGroup = adapter.isUserInGroup(userId, termsAndPoliciesGroupId); + protected void addAcceptedTermsAndPolicies(PassportAssemblyContext ctx) { + String type = TYPE_ACCEPTED_TERMS_AND_POLICIES; + logAddingVisas(type); + if (termsAndPoliciesGroupId == null) { + log.debug("Group ID for accepted terms and policies is not defined, not adding any {} visas", type); + return; + } + + boolean userInGroup = adapter.isUserInGroup(ctx.getPerunUserId(), termsAndPoliciesGroupId); if (!userInGroup) { + log.debug("User is not in the group representing terms and policies approval, not adding any {} visas", type); return; } - long asserted = now; - if (bonaFideStatusAttr != null) { - String bonaFideStatusCreatedAt = adapter.getAdapterRpc().getUserAttributeCreatedAt(userId, bonaFideStatusAttr); + long asserted = ctx.getNow(); + if (StringUtils.hasText(bonaFideStatusAttr)) { + String bonaFideStatusCreatedAt = adapter.getAdapterRpc() + .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusAttr); if (bonaFideStatusCreatedAt != null) { asserted = Timestamp.valueOf(bonaFideStatusCreatedAt).getTime() / 1000L; } } long expires = Utils.getExpires(asserted, 100L); - if (expires < now) { - return; - } - JsonNode visa = createPassportVisa(TYPE_ACCEPTED_TERMS_AND_POLICIES, sub, userId, BONA_FIDE_URL, ELIXIR_ORG_URL, BY_SELF, asserted, expires, null); + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(ELIXIR_ORG_URL) + .by(BY_SELF) + .asserted(asserted) + .expires(expires) + .conditions(null) + .build() + ); + if (visa != null) { - passport.add(visa); + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); } - } @Override - protected void addResearcherStatuses(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId) - { - addResearcherStatusFromBonaFideAttribute(now, passport, sub, userId); - addResearcherStatusFromAffiliation(affiliations, now, passport, sub, userId); - addResearcherStatusGroupAffiliations(now, passport, userId, sub); + protected void addControlledAccessGrants(PassportAssemblyContext ctx) { + String type = TYPE_CONTROLLED_ACCESS_GRANTS; + logAddingVisas(type); + List<Ga4ghPassportVisa> controlledAccessGrants = ctx.getExternalControlledAccessGrants(); + if (controlledAccessGrants == null || controlledAccessGrants.isEmpty()) { + log.debug("No external {} visas available, not adding any {} visas", type, type); + return; + } + ctx.getResultVisas().addAll(controlledAccessGrants); } @Override - protected void addControlledAccessGrants(long now, ArrayNode passport, String sub, Long userId) { - if (claimRepositories.isEmpty()) { - return; - } - Set<String> linkedIdentities = new HashSet<>(); - for (Ga4ghClaimRepository repo: claimRepositories) { - callPermissionsJwtAPI(repo, Collections.singletonMap(ELIXIR_ID, sub), passport, linkedIdentities); - } - if (linkedIdentities.isEmpty()) { + protected void addLinkedIdentities(PassportAssemblyContext ctx) { + String type = TYPE_LINKED_IDENTITIES; + logAddingVisas(type); + Set<String> externalLinkedIdentities = ctx.getExternalLinkedIdentities(); + if (externalLinkedIdentities == null || externalLinkedIdentities.isEmpty()) { + log.debug("No external {} visas available, not adding any {} visas", type, type); return; } - for (String linkedIdentity : linkedIdentities) { - long expires = Utils.getOneYearExpires(now); - JsonNode visa = createPassportVisa(TYPE_LINKED_IDENTITIES, sub, userId, linkedIdentity, - ELIXIR_ORG_URL, BY_SYSTEM, now, expires, null); + for (String identity: externalLinkedIdentities) { + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(identity) + .source(ELIXIR_ORG_URL) + .by(BY_SYSTEM) + .asserted(ctx.getNow()) + .expires(Utils.getOneYearExpires(ctx.getNow())) + .conditions(null) + .build() + ); if (visa != null) { - passport.add(visa); + ctx.getResultVisas().add(visa); + logAddedVisa(type, identity); } } } - private void addResearcherStatusFromBonaFideAttribute(long now, ArrayNode passport, String sub, Long userId) + @Override + protected void addResearcherStatuses(PassportAssemblyContext ctx) { - String elixirBonaFideStatusREMSCreatedAt = adapter.getAdapterRpc().getUserAttributeCreatedAt(userId, bonaFideStatusREMSAttr); + logAddingVisas(TYPE_RESEARCHER_STATUS); + addResearcherStatusFromRemsBonaFideAttribute(ctx); + addResearcherStatusFromAffiliation(ctx); + addResearcherStatusFromGroupAffiliations(ctx); + } + + private void addResearcherStatusFromRemsBonaFideAttribute(PassportAssemblyContext ctx) { + String type = TYPE_RESEARCHER_STATUS; + log.debug("Adding {} visa (from REMS bona fide status)", type); + if (!StringUtils.hasText(bonaFideStatusREMSAttr)) { + log.debug("REMS bonaFideStatus attribute is not defined, not adding any {} visas (from REMS bona fide status)", type); + return; + } + String elixirBonaFideStatusREMSCreatedAt = adapter.getAdapterRpc() + .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusREMSAttr); if (elixirBonaFideStatusREMSCreatedAt == null) { return; } @@ -146,42 +211,70 @@ public class ElixirGa4ghBroker extends Ga4ghBroker { long asserted = Timestamp.valueOf(elixirBonaFideStatusREMSCreatedAt).getTime() / 1000L; long expires = Utils.getOneYearExpires(asserted); - if (expires < now) { - return; - } - - JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, ELIXIR_ORG_URL, BY_PEER, asserted, expires, null); + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(ELIXIR_ORG_URL) + .by(BY_PEER) + .asserted(asserted) + .expires(expires) + .conditions(null) + .build() + ); if (visa != null) { - passport.add(visa); + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); } } - private void addResearcherStatusFromAffiliation(List<Affiliation> affiliations, long now, ArrayNode passport, String sub, Long userId) - { - if (affiliations == null) { + private void addResearcherStatusFromAffiliation(PassportAssemblyContext ctx) { + String type = TYPE_RESEARCHER_STATUS; + log.debug("Adding {} visa (from affiliations)", type); + if (ctx.getIdentityAffiliations() == null || ctx.getIdentityAffiliations().isEmpty()) { + log.debug("No affiliations available, not adding any {} visas (from affiliations)", type); return; } - for (Affiliation affiliation: affiliations) { + for (Affiliation affiliation: ctx.getIdentityAffiliations()) { if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) { continue; } - long expires = Utils.getOneYearExpires(affiliation.getAsserted()); - if (expires < now) { - continue; - } - - JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, affiliation.getSource(), BY_SYSTEM, affiliation.getAsserted(), expires, null); + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(affiliation.getSource()) + .by(BY_SYSTEM) + .asserted(affiliation.getAsserted()) + .expires(Utils.getOneYearExpires(affiliation.getAsserted())) + .conditions(null) + .build() + ); if (visa != null) { - passport.add(visa); + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); } } } - private void addResearcherStatusGroupAffiliations(long now, ArrayNode passport, Long userId, String sub) { - List<Affiliation> groupAffiliations = adapter.getGroupAffiliations(userId, groupAffiliationsAttr); - if (groupAffiliations == null) { + private void addResearcherStatusFromGroupAffiliations(PassportAssemblyContext ctx) { + String type = TYPE_RESEARCHER_STATUS; + log.debug("Adding {} visa (from group affiliations)", type); + if (!StringUtils.hasText(groupAffiliationsAttr)) { + log.debug("GroupAffiliations attribute is not defined, not adding any {} visas (from group affiliations)", type); + return; + } + List<Affiliation> groupAffiliations = adapter.getGroupAffiliations( + ctx.getPerunUserId(), elixirVoId, groupAffiliationsAttr); + if (groupAffiliations == null || groupAffiliations.isEmpty()) { return; } @@ -190,12 +283,27 @@ public class ElixirGa4ghBroker extends Ga4ghBroker { continue; } - long expires = Utils.getOneYearExpires(now); - - JsonNode visa = createPassportVisa(TYPE_RESEARCHER_STATUS, sub, userId, BONA_FIDE_URL, ELIXIR_ORG_URL, BY_SO, affiliation.getAsserted(), expires, null); + long expires = Utils.getOneYearExpires(ctx.getNow()); + + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(ELIXIR_ORG_URL) + .by(BY_SO) + .asserted(affiliation.getAsserted()) + .expires(Utils.getOneYearExpires(affiliation.getAsserted())) + .conditions(null) + .build() + ); if (visa != null) { - passport.add(visa); + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); } } } + } diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/Ga4ghBroker.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/Ga4ghBroker.java index 51e8cc5..df82784 100644 --- a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/Ga4ghBroker.java +++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/Ga4ghBroker.java @@ -2,7 +2,6 @@ package cz.muni.ics.ga4gh.service.impl.brokers; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.JWSAlgorithm; @@ -11,31 +10,33 @@ import com.nimbusds.jose.jwk.source.RemoteJWKSet; import com.nimbusds.jose.proc.SecurityContext; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; -import cz.muni.ics.ga4gh.adapters.PerunAdapter; -import cz.muni.ics.ga4gh.config.BrokerConfig; -import cz.muni.ics.ga4gh.config.Ga4ghConfig; -import cz.muni.ics.ga4gh.model.Affiliation; -import cz.muni.ics.ga4gh.model.Ga4ghClaimRepository; -import cz.muni.ics.ga4gh.model.Ga4ghPassportVisa; +import cz.muni.ics.ga4gh.base.Utils; +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import cz.muni.ics.ga4gh.base.model.Affiliation; +import cz.muni.ics.ga4gh.base.model.Ga4ghClaimRepository; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisaV1; +import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties; +import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties; import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService; -import cz.muni.ics.ga4gh.utils.Utils; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.MediaType; -import org.springframework.web.client.HttpClientErrorException; - +import cz.muni.ics.ga4gh.service.PassportAssemblyContext; +import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters; import java.io.IOException; -import java.net.MalformedURLException; import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; import java.time.Instant; import java.util.ArrayList; -import java.util.Date; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.util.StringUtils; +import org.springframework.web.client.HttpClientErrorException; @Slf4j public abstract class Ga4ghBroker { @@ -43,13 +44,18 @@ public abstract class Ga4ghBroker { public static final String GA4GH_CLAIM = "ga4gh_passport_v1"; public static final String JSON = "json"; + public static final String REPOSITORY_URL_USER_ID = "user_id"; + protected final List<Ga4ghClaimRepository> claimRepositories = new ArrayList<>(); protected final Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets = new HashMap<>(); protected final Map<URI, String> signers = new HashMap<>(); - protected final ObjectMapper mapper = new ObjectMapper(); - private final String issuer; - private final URI jku; + protected final String issuer; + protected final URI jku; + + @Getter + private final String brokerName; + protected PerunAdapter adapter; protected JWTSigningAndValidationService jwtService; @@ -58,119 +64,166 @@ public abstract class Ga4ghBroker { private final String orgUrlAttr; - public Ga4ghBroker(PerunAdapter adapter, JWTSigningAndValidationService jwtService, Ga4ghConfig config, BrokerConfig brokerConfig) throws URISyntaxException, MalformedURLException { + public Ga4ghBroker(BrokerInstanceProperties instanceProperties, + Ga4ghBrokersProperties brokersProperties, + PerunAdapter adapter, + JWTSigningAndValidationService jwtService) + { this.adapter = adapter; + this.affiliationsAttr = instanceProperties.getAffiliationsAttr(); + this.orgUrlAttr = instanceProperties.getOrgUrlAttr(); + this.issuer = brokersProperties.getIssuer(); + this.jku = brokersProperties.getJku(); this.jwtService = jwtService; - this.affiliationsAttr = brokerConfig.getAffiliationsAttr(); - this.orgUrlAttr = brokerConfig.getOrgUrlAttr(); - this.issuer = brokerConfig.getIssuer(); - this.jku = new URL(brokerConfig.getJku()).toURI(); - - Utils.parseConfigFile(config, claimRepositories, remoteJwkSets, signers); + this.brokerName = instanceProperties.getName(); + Utils.initializeClaimRepositories(instanceProperties, claimRepositories, remoteJwkSets, signers); } - public ArrayNode constructGa4ghPassportVisa(Long userId, String sub) { - List<Affiliation> affiliations = adapter.getAdapterRpc().getUserExtSourcesAffiliations(userId, affiliationsAttr, orgUrlAttr); + public List<Ga4ghPassportVisa> constructGa4ghPassportVisas(Long userId) { + if (!isCommunityMember(userId)) { + return new ArrayList<>(); + } + + String communityIdentifier = getUserSub(userId); + + List<Affiliation> identityAffiliations = new ArrayList<>(); + List<Ga4ghPassportVisa> controlledAccessGrantsFromRepositories = new ArrayList<>(); + Set<String> linkedIdentitiesFromRepositories = new HashSet<>(); + + fillIdentityAffiliations(identityAffiliations, userId); + callExternalRepositories(communityIdentifier, controlledAccessGrantsFromRepositories, linkedIdentitiesFromRepositories); - ArrayNode ga4gh_passport_v1 = JsonNodeFactory.instance.arrayNode(); long now = Instant.now().getEpochSecond(); - addAffiliationAndRoles(now, ga4gh_passport_v1, affiliations, sub, userId); - addAcceptedTermsAndPolicies(now, ga4gh_passport_v1, userId, sub); - addResearcherStatuses(now, ga4gh_passport_v1, affiliations, sub, userId); - addControlledAccessGrants(now, ga4gh_passport_v1, sub, userId); + PassportAssemblyContext ctx = PassportAssemblyContext.builder() + .resultVisas(new ArrayList<>()) + .perunAdapter(adapter) + .identityAffiliations(identityAffiliations) + .externalControlledAccessGrants(controlledAccessGrantsFromRepositories) + .externalLinkedIdentities(linkedIdentitiesFromRepositories) + .perunUserId(userId) + .subject(communityIdentifier) + .now(now) + .build(); + + addAffiliationAndRoles(ctx); + addAcceptedTermsAndPolicies(ctx); + addResearcherStatuses(ctx); + addControlledAccessGrants(ctx); + addLinkedIdentities(ctx); + + return ctx.getResultVisas(); + } + + private void callExternalRepositories(String sub, + List<Ga4ghPassportVisa> controlledAccessGrantsFromRepositories, + Set<String> linkedIdentitiesFromRepositories) + { + for (Ga4ghClaimRepository repository: claimRepositories) { + callPermissionsJwtAPI( + repository, + Collections.singletonMap(REPOSITORY_URL_USER_ID, sub), + controlledAccessGrantsFromRepositories, + linkedIdentitiesFromRepositories + ); + } - return ga4gh_passport_v1; } + private void fillIdentityAffiliations(List<Affiliation> affiliationsList, Long userId) { + if (affiliationsList == null) { + affiliationsList = new ArrayList<>(); + } + if (!StringUtils.hasText(affiliationsAttr) || !StringUtils.hasText(orgUrlAttr)) { + log.warn("Affiliations attribute or orgUrl attribute is not defined"); + } else { + List<Affiliation> affiliations = adapter.getAdapterRpc() + .getUserExtSourcesAffiliations(userId, affiliationsAttr, orgUrlAttr); + if (affiliations != null) { + affiliationsList.addAll(affiliations); + } + } + } - protected abstract void addAffiliationAndRoles(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId); + protected abstract String getSubAttribute(); - protected abstract void addAcceptedTermsAndPolicies(long now, ArrayNode passport, Long userId, String sub); + protected abstract Long getCommunityVoId(); - protected abstract void addResearcherStatuses(long now, ArrayNode passport, List<Affiliation> affiliations, String sub, Long userId); + protected abstract void addAffiliationAndRoles(PassportAssemblyContext ctx); - protected abstract void addControlledAccessGrants(long now, ArrayNode passport, String sub, Long userId); + protected abstract void addAcceptedTermsAndPolicies(PassportAssemblyContext ctx); - protected JsonNode createPassportVisa(String type, String sub, Long userId, String value, String source, - String by, long asserted, long expires, JsonNode condition) - { - long now = System.currentTimeMillis() / 1000L; + protected abstract void addResearcherStatuses(PassportAssemblyContext ctx); - if (asserted > now) { - log.warn("Visa asserted in future, it will be ignored!"); - log.debug("Visa information: perunUserId={}, sub={}, type={}, value={}, source={}, by={}, asserted={}", - userId, sub, type, value, source, by, Instant.ofEpochSecond(asserted)); + protected abstract void addControlledAccessGrants(PassportAssemblyContext ctx); - return null; - } + protected abstract void addLinkedIdentities(PassportAssemblyContext ctx); - if (expires <= now) { - log.warn("Visa is expired, it will be ignored!"); - log.debug("Visa information: perunUserId={}, sub={}, type={}, value={}, source={}, by={}, expired={}", - userId, sub, type, value, source, by, Instant.ofEpochSecond(expires)); + protected String getUserSub(Long userId) { + return adapter.getUserSub(userId, getSubAttribute()); + } - return null; + protected boolean isCommunityMember(Long userId) { + Long communityVoId = getCommunityVoId(); + if (communityVoId == null) { + return true; } + return adapter.isUserInVo(userId, communityVoId); + } - Map<String, Object> passportVisaObject = new HashMap<>(); - passportVisaObject.put(Ga4ghPassportVisa.TYPE, type); - passportVisaObject.put(Ga4ghPassportVisa.ASSERTED, asserted); - passportVisaObject.put(Ga4ghPassportVisa.VALUE, value); - passportVisaObject.put(Ga4ghPassportVisa.SOURCE, source); - passportVisaObject.put(Ga4ghPassportVisa.BY, by); + protected void logAddingVisas(String type) { + log.info("Adding '{}' visas", type); + } - if (condition != null && !condition.isNull() && !condition.isMissingNode()) { - passportVisaObject.put(Ga4ghPassportVisa.CONDITION, condition); - } + protected void logAddedVisa(String type, String value) { + log.debug("Added '{}' visa with value '{}'", type, value); + } - JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.parse(jwtService.getDefaultSigningAlgorithm().getName())) - .keyID(jwtService.getDefaultSignerKeyId()) - .type(JOSEObjectType.JWT) - .jwkURL(jku) - .build(); - - JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder() - .issuer(issuer) - .issueTime(new Date()) - .expirationTime(new Date(expires * 1000L)) - .subject(sub) - .jwtID(UUID.randomUUID().toString()) - .claim(Ga4ghPassportVisa.GA4GH_VISA_V1, passportVisaObject) - .build(); - - SignedJWT myToken = new SignedJWT(jwsHeader, jwtClaimsSet); - jwtService.signJwt(myToken); - - return JsonNodeFactory.instance.textNode(myToken.serialize()); + protected Ga4ghPassportVisa createVisa(VisaAssemblyParameters parameters) + { + parameters.setIssuer(issuer); + parameters.setJku(jku); + parameters.setSigner(issuer); + parameters.setJwtService(jwtService); + return Utils.createVisa(parameters); } protected void callPermissionsJwtAPI(Ga4ghClaimRepository repo, Map<String, String> uriVariables, - ArrayNode passport, - Set<String> linkedIdentities) + List<Ga4ghPassportVisa> controlledAccessGrantsList, + Set<String> linkedIdentitiesSet) { - log.debug("GA4GH: {}", uriVariables); + log.debug("Calling claim repository '{}' with parameters '{}'", repo, uriVariables); JsonNode response = callHttpJsonAPI(repo, uriVariables); - if (response != null) { - JsonNode visas = response.path(GA4GH_CLAIM); - if (visas.isArray()) { - for (JsonNode visaNode : visas) { - if (visaNode.isTextual()) { - Ga4ghPassportVisa visa = Utils.parseAndVerifyVisa(visaNode.asText(), signers, remoteJwkSets, mapper); - if (visa.isVerified()) { - log.debug("Adding a visa to passport: {}", visa); - passport.add(passport.textNode(visa.getJwt())); - linkedIdentities.add(visa.getLinkedIdentity()); - } else { - log.warn("Skipping visa: {}", visa); - } - } else { - log.warn("Element of {} is not a String: {}", GA4GH_CLAIM, visaNode); - } - } + if (response == null) { + log.debug("No response returned"); + return; + } else if (!response.hasNonNull(GA4GH_CLAIM)) { + log.debug("Response does not contain non null value for key '{}'", GA4GH_CLAIM); + return; + } + + JsonNode visas = response.path(GA4GH_CLAIM); + if (!visas.isArray()) { + log.warn("'{}' claim is not an array. Received response '{}'", GA4GH_CLAIM, response); + } + for (JsonNode visaNode : visas) { + if (!visaNode.isTextual()) { + log.warn("Element '{}' of '{}' is not a String, skipping value", visaNode, GA4GH_CLAIM); + continue; + } + Ga4ghPassportVisa visa = Utils.parseVisa(visaNode.asText()); + if (visa == null) { + log.debug("Visa '{}' could not be parsed", visaNode); + continue; + } + Utils.verifyVisa(visa, signers, remoteJwkSets); + if (visa.isVerified()) { + log.debug("Adding a visa to passport: {}", visa); + controlledAccessGrantsList.add(visa); + linkedIdentitiesSet.add(visa.getLinkedIdentity()); } else { - log.warn("{} is not an array in {}", GA4GH_CLAIM, response); + log.warn("Skipping visa: {}", visa); } } } @@ -182,24 +235,28 @@ public abstract class Ga4ghBroker { JsonNode result; try { if (log.isDebugEnabled()) { - log.debug("Calling Permissions API at {}", repo.getRestTemplate().getUriTemplateHandler().expand(repo.getActionURL(), uriVariables)); + log.debug("Calling Permissions API at {}", repo.getRestTemplate() + .getUriTemplateHandler().expand(repo.getActionURL(), uriVariables)); } result = repo.getRestTemplate().getForObject(repo.getActionURL(), JsonNode.class, uriVariables); } catch (HttpClientErrorException ex) { - MediaType contentType = ex.getResponseHeaders().getContentType(); + MediaType contentType = null; + if (ex.getResponseHeaders() != null) { + contentType = ex.getResponseHeaders().getContentType(); + } String body = ex.getResponseBodyAsString(); - log.error("HTTP ERROR: {}, URL: {}, Content-Type: {}", ex.getRawStatusCode(), repo.getActionURL(), contentType); + log.error("HTTP ERROR: {}, URL: {}, Content-Type: {}", + ex.getRawStatusCode(), repo.getActionURL(), contentType); if (ex.getRawStatusCode() == 404) { - log.warn("Got status 404 from Permissions endpoint {}, ELIXIR AAI user is not linked to user at Permissions API", + log.warn("Got status 404 from Permissions endpoint {}, user is not linked to user at Permissions API", repo.getActionURL()); - return null; } - if (JSON.equals(contentType.getSubtype())) { + if (contentType != null && JSON.equals(contentType.getSubtype())) { try { log.error(new ObjectMapper().readValue(body, JsonNode.class).path("message").asText()); } catch (IOException e) { @@ -208,16 +265,14 @@ public abstract class Ga4ghBroker { } else { log.error("cannot make REST call, exception: {} message: {}", ex.getClass().getName(), ex.getMessage()); } - return null; } log.debug("Permissions API response: {}", result); - return result; } catch (Exception ex) { log.error("Cannot get dataset permissions", ex); } - return null; } + } diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/LifescienceRiGa4ghBroker.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/LifescienceRiGa4ghBroker.java new file mode 100644 index 0000000..08b30be --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/LifescienceRiGa4ghBroker.java @@ -0,0 +1,330 @@ +package cz.muni.ics.ga4gh.service.impl.brokers; + +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_PEER; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SELF; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SO; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SYSTEM; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_CONTROLLED_ACCESS_GRANTS; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS; + +import cz.muni.ics.ga4gh.base.Utils; +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import cz.muni.ics.ga4gh.base.model.Affiliation; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa; +import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties; +import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties; +import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService; +import cz.muni.ics.ga4gh.service.PassportAssemblyContext; +import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters; +import java.sql.Timestamp; +import java.util.List; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +@Slf4j +public class LifescienceRiGa4ghBroker extends Ga4ghBroker { + + private static final String BONA_FIDE_URL = "https://doi.org/10.1038/s41431-018-0219-y"; + private static final String LS_RI_ORG_URL = "https://lifescience-ri.eu/"; + private static final String FACULTY_AT = "faculty@"; + + private final String elixirIdAttribute; + private final String bonaFideStatusAttr; + private final String bonaFideStatusREMSAttr; + private final String groupAffiliationsAttr; + private final Long termsAndPoliciesGroupId; + + private final Long lifescienceRiVoId; + + public LifescienceRiGa4ghBroker(BrokerInstanceProperties instanceProperties, + Ga4ghBrokersProperties brokersProperties, + PerunAdapter adapter, + JWTSigningAndValidationService jwtService) + { + super(instanceProperties, brokersProperties, adapter, jwtService); + + this.elixirIdAttribute = instanceProperties.getIdentifierAttribute(); + this.bonaFideStatusAttr = instanceProperties.getBonaFideStatusAttr(); + this.bonaFideStatusREMSAttr = instanceProperties.getBonaFideStatusRemsAttr(); + this.groupAffiliationsAttr = instanceProperties.getGroupAffiliationsAttr(); + this.termsAndPoliciesGroupId = instanceProperties.getTermsAndPoliciesGroupId(); + this.lifescienceRiVoId = instanceProperties.getMembershipVoId(); + } + + @Override + protected String getSubAttribute() { + return elixirIdAttribute; + } + + @Override + protected Long getCommunityVoId() { + return lifescienceRiVoId; + } + + @Override + protected void addAffiliationAndRoles(PassportAssemblyContext ctx) + { + String type = TYPE_AFFILIATION_AND_ROLE; + logAddingVisas(type); + if (!isCommunityMember(ctx.getPerunUserId())) { + log.debug("User is not member of the LS community, not adding any {} visas", type); + return; + } + + long asserted = System.currentTimeMillis() / 1000L; + + Affiliation affiliate = new Affiliation(null, "affiliate@lifescience-ri.eu",asserted); + Ga4ghPassportVisa affiliateVisa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(affiliate.getValue()) + .source(affiliate.getSource()) + .by(BY_SYSTEM) + .asserted(affiliate.getAsserted()) + .expires(Utils.getOneYearExpires(affiliate.getAsserted())) + .conditions(null) + .build() + ); + if (affiliateVisa != null) { + ctx.getResultVisas().add(affiliateVisa); + logAddedVisa(type, affiliate.getValue()); + } + + Affiliation member = new Affiliation(null, "member@lifescience-ri.eu",asserted); + Ga4ghPassportVisa memberVisa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(member.getValue()) + .source(affiliate.getSource()) + .by(BY_SYSTEM) + .asserted(affiliate.getAsserted()) + .expires(Utils.getOneYearExpires(affiliate.getAsserted())) + .conditions(null) + .build() + ); + + if (memberVisa != null) { + ctx.getResultVisas().add(memberVisa); + logAddedVisa(type, member.getValue()); + } + } + + @Override + protected void addAcceptedTermsAndPolicies(PassportAssemblyContext ctx) { + String type = TYPE_ACCEPTED_TERMS_AND_POLICIES; + logAddingVisas(type); + if (termsAndPoliciesGroupId == null) { + log.debug("Group ID for accepted terms and policies not defined, not adding any {} visas", type); + return; + } + + boolean userInGroup = adapter.isUserInGroup(ctx.getPerunUserId(), termsAndPoliciesGroupId); + if (!userInGroup) { + log.debug("User is not in the group representing terms and policies approval, not adding any {} visas", type); + return; + } + + long asserted = ctx.getNow(); + if (StringUtils.hasText(bonaFideStatusAttr)) { + String bonaFideStatusCreatedAt = adapter.getAdapterRpc() + .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusAttr); + if (bonaFideStatusCreatedAt != null) { + asserted = Timestamp.valueOf(bonaFideStatusCreatedAt).getTime() / 1000L; + } + } + long expires = Utils.getExpires(asserted, 100L); + + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(LS_RI_ORG_URL) + .by(BY_SELF) + .asserted(asserted) + .expires(expires) + .conditions(null) + .build() + ); + + if (visa != null) { + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); + } + } + + @Override + protected void addControlledAccessGrants(PassportAssemblyContext ctx) { + String type = TYPE_CONTROLLED_ACCESS_GRANTS; + logAddingVisas(type); + List<Ga4ghPassportVisa> controlledAccessGrants = ctx.getExternalControlledAccessGrants(); + if (controlledAccessGrants == null || controlledAccessGrants.isEmpty()) { + log.debug("No external {} visas available, not adding any {} visas", type, type); + return; + } + for (Ga4ghPassportVisa acgVisa: controlledAccessGrants) { + ctx.getResultVisas().add(acgVisa); + logAddedVisa(type, acgVisa.getGa4ghVisaV1().getValue()); + } + } + + @Override + protected void addLinkedIdentities(PassportAssemblyContext ctx) { + String type = TYPE_LINKED_IDENTITIES; + logAddingVisas(type); + Set<String> externalLinkedIdentities = ctx.getExternalLinkedIdentities(); + if (externalLinkedIdentities == null || externalLinkedIdentities.isEmpty()) { + log.debug("No external {} visas available, not adding any {} visas", type, type); + return; + } + for (String identity: externalLinkedIdentities) { + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(identity) + .source(LS_RI_ORG_URL) + .by(BY_SYSTEM) + .asserted(ctx.getNow()) + .expires(Utils.getOneYearExpires(ctx.getNow())) + .conditions(null) + .build() + ); + if (visa != null) { + ctx.getResultVisas().add(visa); + logAddedVisa(type, identity); + } + } + } + + @Override + protected void addResearcherStatuses(PassportAssemblyContext ctx) + { + logAddingVisas(TYPE_RESEARCHER_STATUS); + //addResearcherStatusFromRemsBonaFideAttribute(ctx); - rems not defined yet + addResearcherStatusFromAffiliation(ctx); + addResearcherStatusFromGroupAffiliations(ctx); + } + + private void addResearcherStatusFromRemsBonaFideAttribute(PassportAssemblyContext ctx) + { + String type = TYPE_RESEARCHER_STATUS; + log.debug("Adding {} visa (from REMS bona fide status)", type); + if (!StringUtils.hasText(bonaFideStatusREMSAttr)) { + log.debug("REMS bonaFideStatus attribute is not defined, not adding any {} visas (from REMS bona fide status)", type); + return; + } + + String bonaFideStatusREMSCreatedAt = adapter.getAdapterRpc() + .getUserAttributeCreatedAt(ctx.getPerunUserId(), bonaFideStatusREMSAttr); + if (bonaFideStatusREMSCreatedAt == null) { + return; + } + + long asserted = Timestamp.valueOf(bonaFideStatusREMSCreatedAt).getTime() / 1000L; + long expires = Utils.getOneYearExpires(asserted); + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(LS_RI_ORG_URL) + .by(BY_PEER) + .asserted(asserted) + .expires(expires) + .conditions(null) + .build() + ); + if (visa != null) { + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); + } + } + + private void addResearcherStatusFromAffiliation(PassportAssemblyContext ctx) { + String type = TYPE_RESEARCHER_STATUS; + log.debug("Adding {} visa (from affiliations)", type); + if (ctx.getIdentityAffiliations() == null || ctx.getIdentityAffiliations().isEmpty()) { + log.debug("No affiliations available, not adding any {} visas (from affiliations)", type); + return; + } + + for (Affiliation affiliation: ctx.getIdentityAffiliations()) { + if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) { + continue; + } + + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(affiliation.getSource()) + .by(BY_SYSTEM) + .asserted(affiliation.getAsserted()) + .expires(Utils.getOneYearExpires(affiliation.getAsserted())) + .conditions(null) + .build() + ); + if (visa != null) { + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); + } + } + } + + private void addResearcherStatusFromGroupAffiliations(PassportAssemblyContext ctx) { + String type = TYPE_RESEARCHER_STATUS; + log.debug("Adding {} visa (from group affiliations)", type); + if (!StringUtils.hasText(groupAffiliationsAttr)) { + log.debug("GroupAffiliations attribute is not defined, not adding any {} visas (from group affiliations)", type); + return; + } + List<Affiliation> groupAffiliations = adapter.getGroupAffiliations( + ctx.getPerunUserId(), lifescienceRiVoId, groupAffiliationsAttr); + if (groupAffiliations == null || groupAffiliations.isEmpty()) { + return; + } + + for (Affiliation affiliation: groupAffiliations) { + if (!StringUtils.startsWithIgnoreCase(affiliation.getValue(), FACULTY_AT)) { + continue; + } + + String value = BONA_FIDE_URL; + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(LS_RI_ORG_URL) + .by(BY_SO) + .asserted(affiliation.getAsserted()) + .expires(Utils.getOneYearExpires(affiliation.getAsserted())) + .conditions(null) + .build() + ); + if (visa != null) { + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); + } + } + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/PerunGa4ghBroker.java b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/PerunGa4ghBroker.java new file mode 100644 index 0000000..3b5080b --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/service/impl/brokers/PerunGa4ghBroker.java @@ -0,0 +1,154 @@ +package cz.muni.ics.ga4gh.service.impl.brokers; + +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.BY_SYSTEM; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_ACCEPTED_TERMS_AND_POLICIES; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_AFFILIATION_AND_ROLE; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_CONTROLLED_ACCESS_GRANTS; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_LINKED_IDENTITIES; +import static cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa.TYPE_RESEARCHER_STATUS; + +import com.fasterxml.jackson.databind.JsonNode; +import cz.muni.ics.ga4gh.base.Utils; +import cz.muni.ics.ga4gh.base.adapters.PerunAdapter; +import cz.muni.ics.ga4gh.base.model.Affiliation; +import cz.muni.ics.ga4gh.base.model.Ga4ghPassportVisa; +import cz.muni.ics.ga4gh.base.model.UserExtSource; +import cz.muni.ics.ga4gh.base.properties.BrokerInstanceProperties; +import cz.muni.ics.ga4gh.base.properties.Ga4ghBrokersProperties; +import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService; +import cz.muni.ics.ga4gh.service.PassportAssemblyContext; +import cz.muni.ics.ga4gh.service.impl.VisaAssemblyParameters; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.validation.constraints.NotBlank; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; + +@Slf4j +@Validated +public class PerunGa4ghBroker extends Ga4ghBroker { + + @NotBlank + private final String idAttribute; + + @NotBlank + private final String source; + + private final Set<String> ignoreLastAccessIdps = new HashSet<>(); + + public PerunGa4ghBroker(BrokerInstanceProperties instanceProperties, + Ga4ghBrokersProperties brokersProperties, + PerunAdapter adapter, + JWTSigningAndValidationService jwtService) { + super(instanceProperties, brokersProperties, adapter, jwtService); + this.idAttribute = instanceProperties.getIdentifierAttribute(); + this.source = instanceProperties.getSource(); + if (instanceProperties.getWhitelistedLinkedIdentitySources() != null) { + this.ignoreLastAccessIdps.addAll( + instanceProperties.getWhitelistedLinkedIdentitySources()); + } + } + + @Override + protected String getSubAttribute() { + return idAttribute; + } + + @Override + protected Long getCommunityVoId() { + return null; + } + + @Override + protected void addAffiliationAndRoles(PassportAssemblyContext ctx) + { + String type = TYPE_AFFILIATION_AND_ROLE; + logAddingVisas(type); + + if (ctx.getIdentityAffiliations() == null || ctx.getIdentityAffiliations().isEmpty()) { + log.debug("No affiliations available, not adding any visas"); + return; + } + + for (Affiliation affiliation: ctx.getIdentityAffiliations()) { + long expires = Utils.getOneYearExpires(affiliation.getAsserted()); + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(affiliation.getValue()) + .source(affiliation.getSource()) + .by(BY_SYSTEM) + .asserted(affiliation.getAsserted()) + .expires(expires) + .conditions(null) + .build() + ); + + if (visa != null) { + ctx.getResultVisas().add(visa); + logAddedVisa(type, affiliation.getValue()); + } + } + } + + @Override + protected void addAcceptedTermsAndPolicies(PassportAssemblyContext ctx) { + logAddingVisas(TYPE_ACCEPTED_TERMS_AND_POLICIES); + // no policies - extend with Perun AUP? + } + + @Override + protected void addControlledAccessGrants(PassportAssemblyContext ctx) { + logAddingVisas(TYPE_CONTROLLED_ACCESS_GRANTS); + // no repositories + } + + @Override + protected void addLinkedIdentities(PassportAssemblyContext ctx) { + String type = TYPE_LINKED_IDENTITIES; + logAddingVisas(type); + + List<UserExtSource> userExtSources = adapter.getAdapterRpc().getIdpUserExtSources(ctx.getPerunUserId()); + for (UserExtSource ues: userExtSources) { + long asserted = ues.getLastAccess().getTime() / 1000L; + long expires = Utils.getOneYearExpires(asserted); + + String idp = ues.getExtSource().getName(); + if (ignoreLastAccessIdps.contains(idp)) { + expires = Utils.getExpires(asserted, 100L); + } + String value = Utils.constructLinkedIdentity(ues.getLogin(), ues.getExtSource().getName()); + Ga4ghPassportVisa visa = createVisa( + VisaAssemblyParameters.builder() + .type(type) + .sub(ctx.getSubject()) + .userId(ctx.getPerunUserId()) + .value(value) + .source(this.source) + .by(BY_SYSTEM) + .asserted(asserted) + .expires(expires) + .conditions(null) + .build() + ); + + if (visa != null) { + ctx.getResultVisas().add(visa); + logAddedVisa(type, value); + } + } + } + + @Override + protected void addResearcherStatuses(PassportAssemblyContext ctx) + { + logAddingVisas(TYPE_RESEARCHER_STATUS); + // Perun does not have researcher status defined + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/utils/Utils.java b/src/main/java/cz/muni/ics/ga4gh/utils/Utils.java deleted file mode 100644 index 7037cb9..0000000 --- a/src/main/java/cz/muni/ics/ga4gh/utils/Utils.java +++ /dev/null @@ -1,218 +0,0 @@ -package cz.muni.ics.ga4gh.utils; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jose.Payload; -import com.nimbusds.jose.crypto.RSASSAVerifier; -import com.nimbusds.jose.jwk.JWK; -import com.nimbusds.jose.jwk.JWKMatcher; -import com.nimbusds.jose.jwk.JWKSelector; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.source.RemoteJWKSet; -import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jwt.JWTParser; -import com.nimbusds.jwt.SignedJWT; -import cz.muni.ics.ga4gh.config.Ga4ghConfig; -import cz.muni.ics.ga4gh.model.Ga4ghClaimRepository; -import cz.muni.ics.ga4gh.model.Ga4ghPassportVisa; -import cz.muni.ics.ga4gh.model.Repo; -import cz.muni.ics.ga4gh.model.RepoHeader; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.InterceptingClientHttpRequestFactory; -import org.springframework.web.client.RestTemplate; - -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@Slf4j -public class Utils { - - public static void parseConfigFile(Ga4ghConfig config, - List<Ga4ghClaimRepository> claimRepositories, - Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets, - Map<URI, String> signers) - { - for (Repo repo : config.getRepos()) { - initializeRepo(repo, claimRepositories); - initializeSigner(signers, remoteJwkSets, repo.getName(), repo.getJwks()); - } - } - - public static Ga4ghPassportVisa parseAndVerifyVisa(String jwtString, - Map<URI, String> signers, - Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets, - ObjectMapper mapper) - { - Ga4ghPassportVisa visa = new Ga4ghPassportVisa(jwtString); - - try { - SignedJWT signedJWT = (SignedJWT) JWTParser.parse(jwtString); - URI jku = signedJWT.getHeader().getJWKURL(); - - if (jku == null) { - log.error("JKU is missing in JWT header"); - return visa; - } - - visa.setSigner(signers.get(jku)); - RemoteJWKSet<SecurityContext> remoteJWKSet = remoteJwkSets.get(jku); - - if (remoteJWKSet == null) { - log.error("JKU '{}' is not among trusted key sets", jku); - return visa; - } - - List<JWK> keys = remoteJWKSet.get(new JWKSelector( - new JWKMatcher.Builder().keyID(signedJWT.getHeader().getKeyID()).build()), null); - - RSASSAVerifier verifier = new RSASSAVerifier(((RSAKey) keys.get(0)).toRSAPublicKey()); - visa.setVerified(signedJWT.verify(verifier)); - - if (visa.isVerified()) { - Utils.processPayload(mapper, visa, signedJWT.getPayload()); - } - } catch (Exception ex) { - log.error("Visa '{}' cannot be parsed and verified", jwtString, ex); - } - return visa; - } - - public static void processPayload(ObjectMapper mapper, Ga4ghPassportVisa visa, Payload payload) - throws IOException - { - JsonNode doc = mapper.readValue(payload.toString(), JsonNode.class); - checkVisaKey(visa, doc, Ga4ghPassportVisa.SUB); - checkVisaKey(visa, doc, Ga4ghPassportVisa.EXP); - checkVisaKey(visa, doc, Ga4ghPassportVisa.ISS); - - JsonNode visa_v1 = doc.path(Ga4ghPassportVisa.GA4GH_VISA_V1); - if (visa_v1.isMissingNode() || visa_v1.isNull() || visa_v1.isEmpty()) { - log.warn("Nothing available in '{}', considering visa as not verified", Ga4ghPassportVisa.GA4GH_VISA_V1); - visa.setVerified(false); - return; - } - - checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.TYPE); - checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.ASSERTED); - checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.VALUE); - checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.SOURCE); - checkVisaKey(visa, visa_v1, Ga4ghPassportVisa.BY); - - if (!visa.isVerified()) { - return; - } - - long exp = doc.get(Ga4ghPassportVisa.EXP).asLong(); - if (exp < Instant.now().getEpochSecond()) { - log.warn("visa expired on {}", isoDateTime(exp)); - visa.setVerified(false); - return; - } - - visa.setLinkedIdentity(URLEncoder.encode(doc.get(Ga4ghPassportVisa.SUB).asText(), StandardCharsets.UTF_8) + - ',' + URLEncoder.encode(doc.get(Ga4ghPassportVisa.ISS).asText(), StandardCharsets.UTF_8)); - - visa.setPrettyPayload( - visa_v1.get(Ga4ghPassportVisa.TYPE).asText() + ": '" - + visa_v1.get(Ga4ghPassportVisa.VALUE).asText() + "' asserted at '" - + isoDate(visa_v1.get(Ga4ghPassportVisa.ASSERTED).asLong()) + '\'' - ); - } - - public static long getOneYearExpires(long asserted) { - return getExpires(asserted, 1L); - } - - public static long getExpires(long asserted, long addYears) { - return Instant.ofEpochSecond(asserted).atZone(ZoneId.systemDefault()).plusYears(addYears).toEpochSecond(); - } - - private static void initializeSigner(Map<URI, String> signers, - Map<URI, RemoteJWKSet<SecurityContext>> remoteJwkSets, - String name, - String jwks) - { - try { - URL jku = new URL(jwks); - remoteJwkSets.put(jku.toURI(), new RemoteJWKSet<>(jku)); - signers.put(jku.toURI(), name); - - log.info("JWKS Signer '{}' added with keys '{}'", name, jwks); - } catch (MalformedURLException | URISyntaxException e) { - log.error("cannot add to RemoteJWKSet map: '{}' -> '{}'", name, jwks, e); - } - } - - private static void initializeRepo(Repo repo, List<Ga4ghClaimRepository> claimRepositories) { - String name = repo.getName(); - String actionURL = repo.getUrl(); - List<RepoHeader> headers = repo.getHeaders(); - - if (actionURL == null || headers.isEmpty()) { - log.error("claim repository '{}' not defined with url|auth_header|auth_value", repo); - return; - } - - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setRequestFactory( - new InterceptingClientHttpRequestFactory(restTemplate.getRequestFactory(), getClientHttpRequestInterceptors(headers)) - ); - - claimRepositories.add(new Ga4ghClaimRepository(name, actionURL, restTemplate)); - log.info("GA4GH Claims Repository '{}' configured at '{}'", name, actionURL); - } - - private static void checkVisaKey(Ga4ghPassportVisa visa, JsonNode jsonNode, String key) { - if (jsonNode.path(key).isMissingNode()) { - log.warn("Key '{}' is missing in the Visa, therefore cannot be verified", key); - visa.setVerified(false); - } else { - switch (key) { - case Ga4ghPassportVisa.SUB: - visa.setSub(jsonNode.path(key).asText()); - break; - case Ga4ghPassportVisa.ISS: - visa.setIss(jsonNode.path(key).asText()); - break; - case Ga4ghPassportVisa.TYPE: - visa.setType(jsonNode.path(key).asText()); - break; - case Ga4ghPassportVisa.VALUE: - visa.setValue(jsonNode.path(key).asText()); - break; - default: - log.warn("Unknown visa key: {}", key); - } - } - } - - private static String isoDate(long linuxTime) { - return isoFormat(linuxTime, DateTimeFormatter.ISO_LOCAL_DATE); - } - - private static String isoDateTime(long linuxTime) { - return isoFormat(linuxTime, DateTimeFormatter.ISO_DATE_TIME); - } - - private static String isoFormat(long linuxTime, DateTimeFormatter formatter) { - ZonedDateTime zdt = ZonedDateTime.ofInstant(Instant.ofEpochSecond(linuxTime), ZoneId.systemDefault()); - return formatter.format(zdt); - } - - private static List<ClientHttpRequestInterceptor> getClientHttpRequestInterceptors(List<RepoHeader> headers) { - return new ArrayList<>(headers); - } -} diff --git a/src/main/java/cz/muni/ics/ga4gh/web/controllers/ExceptionTranslator.java b/src/main/java/cz/muni/ics/ga4gh/web/controllers/ExceptionTranslator.java new file mode 100644 index 0000000..10b950a --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/web/controllers/ExceptionTranslator.java @@ -0,0 +1,29 @@ +package cz.muni.ics.ga4gh.web.controllers; + +import cz.muni.ics.ga4gh.base.exceptions.InvalidRequestParametersException; +import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException; +import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class ExceptionTranslator { + + @ExceptionHandler({InvalidRequestParametersException.class, UserNotUniqueException.class}) + public ResponseEntity<Object> badRequest(Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler({UserNotFoundException.class}) + public ResponseEntity<Object> notFound(Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.NOT_FOUND); + } + + @ExceptionHandler({Exception.class}) + public ResponseEntity<Object> exception(Exception e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/web/controllers/Ga4ghBrokerController.java b/src/main/java/cz/muni/ics/ga4gh/web/controllers/Ga4ghBrokerController.java new file mode 100644 index 0000000..70c89e0 --- /dev/null +++ b/src/main/java/cz/muni/ics/ga4gh/web/controllers/Ga4ghBrokerController.java @@ -0,0 +1,39 @@ +package cz.muni.ics.ga4gh.web.controllers; + +import static cz.muni.ics.ga4gh.web.security.WebSecurityConfigurer.GA4GH_ENDPOINTS_PATH; + +import com.fasterxml.jackson.databind.JsonNode; +import cz.muni.ics.ga4gh.base.exceptions.InvalidRequestParametersException; +import cz.muni.ics.ga4gh.base.exceptions.UserNotFoundException; +import cz.muni.ics.ga4gh.base.exceptions.UserNotUniqueException; +import cz.muni.ics.ga4gh.facade.Ga4ghBrokerFacade; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(GA4GH_ENDPOINTS_PATH) +public class Ga4ghBrokerController { + + private final Ga4ghBrokerFacade ga4GhBrokerFacade; + + @Autowired + public Ga4ghBrokerController(Ga4ghBrokerFacade ga4ghBrokerFacade) { + this.ga4GhBrokerFacade = ga4ghBrokerFacade; + } + + @GetMapping(value = "/{user_id}", produces = MediaType.APPLICATION_JSON_VALUE) + public JsonNode getGa4ghPassport(@PathVariable(name = "user_id") String userIdentifier) + throws UserNotFoundException, UserNotUniqueException, InvalidRequestParametersException + { + if (!StringUtils.hasText(userIdentifier)) { + throw new InvalidRequestParametersException("No user identifier specified"); + } + return ga4GhBrokerFacade.getGa4ghPassport(userIdentifier); + } + +} diff --git a/src/main/java/cz/muni/ics/ga4gh/controllers/JwkSetPublishingEndpoint.java b/src/main/java/cz/muni/ics/ga4gh/web/controllers/JwkSetPublishingEndpoint.java similarity index 77% rename from src/main/java/cz/muni/ics/ga4gh/controllers/JwkSetPublishingEndpoint.java rename to src/main/java/cz/muni/ics/ga4gh/web/controllers/JwkSetPublishingEndpoint.java index 90bd9c7..16e6b67 100644 --- a/src/main/java/cz/muni/ics/ga4gh/controllers/JwkSetPublishingEndpoint.java +++ b/src/main/java/cz/muni/ics/ga4gh/web/controllers/JwkSetPublishingEndpoint.java @@ -1,29 +1,26 @@ -package cz.muni.ics.ga4gh.controllers; +package cz.muni.ics.ga4gh.web.controllers; + +import static cz.muni.ics.ga4gh.web.security.WebSecurityConfigurer.PUBLIC_ENDPOINTS_PATH; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import cz.muni.ics.ga4gh.service.JWTSigningAndValidationService; -import lombok.Getter; -import lombok.Setter; +import java.util.ArrayList; +import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.ArrayList; -import java.util.Map; - @CrossOrigin(originPatterns = "*") @RestController -@RequestMapping("/public") -@Getter -@Setter +@RequestMapping(PUBLIC_ENDPOINTS_PATH) public class JwkSetPublishingEndpoint { public static final String URL = "jwk"; - private JWTSigningAndValidationService jwtService; + private final JWTSigningAndValidationService jwtService; @Autowired public JwkSetPublishingEndpoint(JWTSigningAndValidationService jwtService) { @@ -33,7 +30,7 @@ public class JwkSetPublishingEndpoint { @RequestMapping(value = "/" + URL, produces = MediaType.APPLICATION_JSON_VALUE) public String getJwk() { // map from key id to key - Map<String, JWK> keys = jwtService.getAllPublicKeys(); + Map<String, JWK> keys = jwtService.getPublicKeys(); JWKSet jwkSet = new JWKSet(new ArrayList<>(keys.values())); return jwkSet.toString(); } diff --git a/src/main/java/cz/muni/ics/ga4gh/security/WebSecurityConfigurer.java b/src/main/java/cz/muni/ics/ga4gh/web/security/WebSecurityConfigurer.java similarity index 51% rename from src/main/java/cz/muni/ics/ga4gh/security/WebSecurityConfigurer.java rename to src/main/java/cz/muni/ics/ga4gh/web/security/WebSecurityConfigurer.java index 20d62d1..13e5dae 100644 --- a/src/main/java/cz/muni/ics/ga4gh/security/WebSecurityConfigurer.java +++ b/src/main/java/cz/muni/ics/ga4gh/web/security/WebSecurityConfigurer.java @@ -1,10 +1,13 @@ -package cz.muni.ics.ga4gh.security; +package cz.muni.ics.ga4gh.web.security; -import cz.muni.ics.ga4gh.config.BasicAuthConfig; +import cz.muni.ics.ga4gh.base.model.BasicAuthCredentials; +import cz.muni.ics.ga4gh.base.properties.BasicAuthProperties; +import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @@ -13,30 +16,35 @@ import org.springframework.security.web.SecurityFilterChain; public class WebSecurityConfigurer { private static final String ROLE_USER = "ROLE_USER"; - private static final String PUBLIC_ENDPOINTS_PREFIX = "/public/**"; - private final String username; - private final String password; + public static final String PUBLIC_ENDPOINTS_PATH = "/public"; + public static final String GA4GH_ENDPOINTS_PATH = "/ga4gh"; + private static final String PUBLIC_ENDPOINTS_PREFIX = PUBLIC_ENDPOINTS_PATH + "/**"; + + private final List<BasicAuthCredentials> basicAuthCredentialsList; private final PasswordEncoder passwordEncoder; @Autowired - public WebSecurityConfigurer(BasicAuthConfig config, PasswordEncoder passwordEncoder) { - this.username = config.getUsername(); - this.password = config.getPassword(); + public WebSecurityConfigurer(BasicAuthProperties basicAuthProperties, + PasswordEncoder passwordEncoder) + { + this.basicAuthCredentialsList = basicAuthProperties.getCredentials(); this.passwordEncoder = passwordEncoder; } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication() - .withUser(username).password(passwordEncoder.encode(password)) + InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> configurer = auth.inMemoryAuthentication(); + for (BasicAuthCredentials credentials: basicAuthCredentialsList) { + configurer.withUser(credentials.getUsername()) + .password(passwordEncoder.encode(credentials.getPassword())) .authorities(ROLE_USER); + } } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http.authorizeRequests() .antMatchers(PUBLIC_ENDPOINTS_PREFIX).permitAll() .anyRequest().authenticated() diff --git a/src/main/resources/application-bbmri.yml b/src/main/resources/application-bbmri.yml deleted file mode 100644 index 39cd3d1..0000000 --- a/src/main/resources/application-bbmri.yml +++ /dev/null @@ -1,77 +0,0 @@ ---- - -spring: - profiles: bbmri - -broker: - bona-fide-status-attr: bona_fide_status - bona-fide-status-rems-attr: bona_fide_status_rems - group-affiliations-attr: groupAffiliations - terms-and-policies-group-id: 1 - affiliations-attr: affiliations - org-url-attr: orgUrl - ext-source-name: ext-source-name - issuer: issuer - path-to-jwk-file: path -adapter: - adapter-primary: rpc - call-fallback: false -ldap: - host: host - user: user - password: password - base-dn: dn - use-tls: false - use-ssl: true - allow-untrusted-ssl: false - timeout-secs: 10 - port: 636 -rpc: - enabled: true - url: url - username: username - password: password - serializer: json -attributes: - attributeMappings: - bona_fide_status: - internal-name: bona_fide_status - rpc-name: urn:perun:user:attribute-def:def:bonaFideStatus - ldap-name: bonaFideStatus - bona_fide_status_rems: - internal-name: bona_fide_status_rems - rpc-name: urn:perun:user:attribute-def:def:elixirBonaFideStatusREMS - ldap-name: bonaFideStatusREMS - groupAffiliations: - internal-name: groupAffiliations - rpc-name: urn:perun:group:attribute-def:def:groupAffiliations - ldap-name: groupAffiliations - affiliations: - internal-name: affiliations - rpc-name: urn:perun:ues:attribute-def:def:affiliation - ldap-name: - orgUrl: - internal-name: orgUrl - rpc-name: urn:perun:ues:attribute-def:def:organizationURL - ldap-name: -ga4gh: - repos: - - - name: repo1 - url: url1 - jwks: jwks1 - headers: - - - header: header - value: value - - - name: repo2 - url: url2 - jwks: jwks2 - headers: - - - header: header - value: value -basic-auth: - username: Honza - password: Lojza diff --git a/src/main/resources/application-elixir.yml b/src/main/resources/application-elixir.yml deleted file mode 100644 index 881f452..0000000 --- a/src/main/resources/application-elixir.yml +++ /dev/null @@ -1,92 +0,0 @@ ---- - -spring: - config: - activate: - on-profile: elixir - mvc: - pathmatch: - matching-strategy: ant_path_matcher - -broker: - bona-fide-status-attr: bona_fide_status - bona-fide-status-rems-attr: bona_fide_status_rems - group-affiliations-attr: groupAffiliations - terms-and-policies-group-id: 1 - affiliations-attr: affiliations - org-url-attr: orgUrl - attributes-to-search: - - "elixir-persistent-shadow" - issuer: issuer - jku: jku-url - path-to-jwk-file: path -adapter: - adapter-primary: ldap - call-fallback: false -ldap: - host: host - user: user - password: password - base-dn: dn - use-tls: false - use-ssl: true - allow-untrusted-ssl: false - timeout-secs: 10 - port: 636 -rpc: - enabled: true - url: url - username: username - password: password - serializer: json -attributes: - attributeMappings: - bona_fide_status: - internal-name: bona_fide_status - rpc-name: name - ldap-name: name - bona_fide_status_rems: - internal-name: bona_fide_status_rems - rpc-name: name - ldap-name: name - groupAffiliations: - internal-name: groupAffiliations - rpc-name: name - ldap-name: name - affiliations: - internal-name: affiliations - rpc-name: name - ldap-name: name - orgUrl: - internal-name: orgUrl - rpc-name: name - ldap-name: name - elixir-persistent-shadow: - internal-name: elixir-persistent-shadow - rpc-name: name - ldap-name: name - preferred-mail: - internal-name: preferred-mail - rpc-name: name - ldap-name: name -ga4gh: - repos: - - - name: repo1 - url: url1 - jwks: jwks1 - headers: - - - header: header - value: value - - - name: repo2 - url: url2 - jwks: jwks2 - headers: - - - header: header - value: value -basic-auth: - username: username - password: password diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 63d98d7..dd9c58c 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,11 +3,137 @@ spring: main: allow-bean-definition-overriding: true - profiles: - active: elixir + mvc: + pathmatch: + matching-strategy: ant_path_matcher logging: file: - path: /var/lib/tomcat9/logs/broker.log + path: /var/lib/tomcat9/logs/ga4gh-passport-broker + name: broker.log level: - root: debug \ No newline at end of file + root: error + org.springframework: warn + cz.muni.ics.ga4gh: debug + + +broker: + user-identification-attributes: + - "elixir-persistent-shadow" + issuer: issuer + jku: jku-url + path-to-jwk-file: path + brokers: + - name: elixir + broker-class: ElixirGa4ghBroker + bona-fide-status-attr: bona_fide_status + bona-fide-status-rems-attr: bona_fide_status_rems + group-affiliations-attr: groupAffiliations + terms-and-policies-group-id: 1 + affiliations-attr: affiliations + org-url-attr: orgUrl + identifier-attribute: elixir-persistent-shadow + membership-vo-id: 1 + passport-repositories: + - name: repo1 + url: url1 + jwks: jwks1 + headers: + - header: header + value: value + - name: repo2 + url: url2 + jwks: jwks2 + headers: + - header: header + value: value + - name: bbmri + broker-class: BbmriGa4ghBroker + bona-fide-status-attr: bona_fide_status + bona-fide-status-rems-attr: bona_fide_status_rems + group-affiliations-attr: groupAffiliations + terms-and-policies-group-id: 2 + affiliations-attr: affiliations + org-url-attr: orgUrl + identifier-attribute: bbmri-persistent-shadow + membership-vo-id: 2 + passport-repositories: + - name: repo1 + url: url1 + jwks: jwks1 + headers: + - header: header + value: value + - name: repo2 + url: url2 + jwks: jwks2 + headers: + - header: header + value: value + - name: perun + broker-class: PerunGa4ghBroker + identifier-attribute: lifescience-id + source: https://perun-aai.org/ + affiliations-attr: affiliations + org-url-attr: org_url + whitelisted-linked-identity-sources: + - "https://login.elixir-czech.org/idp/" + - "https://proxy.aai.lifescience-ri.eu/proxy" + +perun: + adapter: + primary: ldap + call-fallback: false + connector: + ldap: + host: host + user: user + password: password + base-dn: dn + use-tls: false + use-ssl: true + allow-untrusted-ssl: false + timeout-secs: 10 + port: 636 + rpc: + enabled: true + url: url + username: username + password: password + serializer: json + +attributes: + attribute_mappings: + bona_fide_status: + internal-name: bona_fide_status + rpc-name: name + ldap-name: name + bona_fide_status_rems: + internal-name: bona_fide_status_rems + rpc-name: name + ldap-name: name + groupAffiliations: + internal-name: groupAffiliations + rpc-name: name + ldap-name: name + affiliations: + internal-name: affiliations + rpc-name: name + ldap-name: name + orgUrl: + internal-name: orgUrl + rpc-name: name + ldap-name: name + elixir-persistent-shadow: + internal-name: elixir-persistent-shadow + rpc-name: name + ldap-name: name + preferred-mail: + internal-name: preferred-mail + rpc-name: name + ldap-name: name + +basic-auth: + credentials: + - username: username + password: password -- GitLab