Skip to content

Commit

Permalink
feat: Add /registry endpoint to the Cloud Gateway (#3076)
Browse files Browse the repository at this point in the history
Signed-off-by: alexandr cumarav <[email protected]>
Signed-off-by: sj895092 <[email protected]>
Signed-off-by: JirkaAichler <[email protected]>
Co-authored-by: sj895092 <[email protected]>
Co-authored-by: ShobhaJayanna <[email protected]>
Co-authored-by: Jiri Aichler <[email protected]>
Co-authored-by: JirkaAichler <[email protected]>
Co-authored-by: Pavel Jareš <[email protected]>
  • Loading branch information
6 people authored Oct 4, 2023
1 parent f0ac1e7 commit ff8ee9b
Show file tree
Hide file tree
Showing 21 changed files with 973 additions and 62 deletions.
70 changes: 70 additions & 0 deletions .github/workflows/integration-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,76 @@ jobs:
- uses: ./.github/actions/teardown

CloudGatewayCentralRegistry:
needs: PublishJibContainers
runs-on: ubuntu-latest
container: ubuntu:latest
timeout-minutes: 10

services:
# First group of services represents central apiml instance with central gateway registry
discovery-service:
image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }}
gateway-service:
image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }}
env:
APIML_SERVICE_APIMLID: central-apiml
APIML_SERVICE_HOSTNAME: gateway-service
cloud-gateway-service:
image: ghcr.io/balhar-jakub/cloud-gateway-service:${{ github.run_id }}-${{ github.run_number }}
env:
APIML_SERVICE_APIMLID: central-apiml
APIML_CLOUDGATEWAY_REGISTRY_ENABLED: true
APIML_SECURITY_X509_REGISTRY_ALLOWEDUSERS: USER,UNKNOWNUSER
discoverable-client:
image: ghcr.io/balhar-jakub/discoverable-client:${{ github.run_id }}-${{ github.run_number }}
mock-services:
image: ghcr.io/balhar-jakub/mock-services:${{ github.run_id }}-${{ github.run_number }}

# Second group of services represents domain apiml instance which registers it's gateway in central's discovery service
discovery-service-2:
image: ghcr.io/balhar-jakub/discovery-service:${{ github.run_id }}-${{ github.run_number }}
volumes:
- /api-defs:/api-defs
env:
APIML_SERVICE_HOSTNAME: discovery-service-2
APIML_SERVICE_PORT: 10031
gateway-service-2:
image: ghcr.io/balhar-jakub/gateway-service:${{ github.run_id }}-${{ github.run_number }}
env:
APIML_SERVICE_APIMLID: domain-apiml
APIML_SERVICE_HOSTNAME: gateway-service-2
APIML_SERVICE_PORT: 10037
APIML_SERVICE_DISCOVERYSERVICEURLS: https://discovery-service-2:10031/eureka/
APIML_SERVICE_CENTRALREGISTRYURLS: https://discovery-service:10011/eureka

steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}

- uses: ./.github/actions/setup

- name: Run CI Tests
run: >
./gradlew :integration-tests:runCloudGatewayCentralRegistryTest --info -Denvironment.config=-docker -Denvironment.offPlatform=true
-Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }}
- name: Dump CGW jacoco data
run: >
java -jar ./scripts/jacococli.jar dump --address cloud-gateway-service --port 6310 --destfile ./results/cloud-gateway-service.exec
- name: Store results
uses: actions/upload-artifact@v2
if: always()
with:
name: CloudGatewayCentralRegistry-${{ env.JOB_ID }}
path: |
integration-tests/build/reports/**
results/**
- uses: ./.github/actions/teardown

CITestsRegistration:
needs: PublishJibContainers
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions cloud-gateway-service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ dependencies {
implementation libs.bcpkix
implementation libs.nimbusJoseJwt
implementation libs.janino
implementation libs.spring.doc
implementation libs.swagger3.parser

compileOnly libs.lombok
annotationProcessor libs.lombok
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,90 @@

package org.zowe.apiml.cloudgatewayservice.config;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.Customizer;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.preauth.x509.SubjectDnX509PrincipalExtractor;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.zowe.apiml.product.constants.CoreService;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;


@Configuration
public class WebSecurity {

@Value("${apiml.security.x509.registry.allowedUsers:#{null}}")
private String allowedUsers;

private Predicate<String> usernameAuthorizationTester;

@PostConstruct
void initScopes() {
boolean authorizeAnyUsers = "*".equals(allowedUsers);

Set<String> users = Optional.ofNullable(allowedUsers)
.map(line -> line.split("[,;]"))
.map(Arrays::asList)
.orElse(Collections.emptyList())
.stream().map(String::trim)
.map(String::toLowerCase)
.collect(Collectors.toSet());

usernameAuthorizationTester = user -> authorizeAnyUsers || users.contains(StringUtils.lowerCase(user));
}

@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.x509(Customizer.withDefaults()).csrf().disable()
.authorizeExchange(exchange -> exchange.anyExchange().permitAll());

SubjectDnX509PrincipalExtractor principalExtractor = new SubjectDnX509PrincipalExtractor();

ReactiveAuthenticationManager authenticationManager = authentication -> {
authentication.setAuthenticated(true);
return Mono.just(authentication);
};

http.x509(x509 ->
x509
.principalExtractor(principalExtractor)
.authenticationManager(authenticationManager)).authorizeExchange()
.pathMatchers("/" + CoreService.CLOUD_GATEWAY.getServiceId() + "/api/v1/registry/**").authenticated()
.and().csrf().disable()
.authorizeExchange().anyExchange().permitAll();

return http.build();
}

@Bean
@Primary
ReactiveUserDetailsService userDetailsService() {
return username -> Mono.just(User.withUsername(username).password("password").authorities("user").build());
}

return username -> {
List<GrantedAuthority> authorities = new ArrayList<>();
if (usernameAuthorizationTester.test(username)) {
authorities.add(new SimpleGrantedAuthority("REGISTRY"));
}
UserDetails userDetails = User.withUsername(username).authorities(authorities).password("").build();
return Mono.just(userDetails);
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,11 @@
package org.zowe.apiml.cloudgatewayservice.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zowe.apiml.cloudgatewayservice.service.CertificateChainService;
import reactor.core.publisher.Mono;

/**
* This simple controller provides a public endpoint with the client certificate chain.
Expand All @@ -30,7 +29,7 @@ public class CertificatesRestController {
private final CertificateChainService certificateChainService;

@GetMapping
public ResponseEntity<String> getCertificates() {
return new ResponseEntity<>(certificateChainService.getCertificatesInPEMFormat(), HttpStatus.OK);
public Mono<String> getCertificates() {
return Mono.just(certificateChainService.getCertificatesInPEMFormat());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.cloudgatewayservice.controller;

import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.zowe.apiml.cloudgatewayservice.service.CentralApimlInfoMapper;
import org.zowe.apiml.cloudgatewayservice.service.GatewayIndexService;
import org.zowe.apiml.cloudgatewayservice.service.model.ApimlInfo;
import org.zowe.apiml.services.ServiceInfo;
import reactor.core.publisher.Flux;

import java.util.List;
import java.util.Map;

import static com.google.common.base.Strings.emptyToNull;

@Slf4j
@RequiredArgsConstructor
@RestController
@Tag(name = "Central Registry")
@RequestMapping(value = "cloud-gateway/api/v1", produces = MediaType.APPLICATION_JSON_VALUE)
@ConditionalOnProperty(value = "apiml.cloudGateway.registry.enabled", havingValue = "true")
public class RegistryController {

private final CentralApimlInfoMapper centralApimlInfoMapper;
private final GatewayIndexService gatewayIndexService;

@GetMapping(value = {"/registry", "/registry/{apimlId}"})
public Flux<ApimlInfo> getServices(@PathVariable(required = false) String apimlId, @RequestParam(name = "apiId", required = false) String apiId, @RequestParam(name = "serviceId", required = false) String serviceId) {
Map<String, List<ServiceInfo>> apimlList = gatewayIndexService.listRegistry(emptyToNull(apimlId), emptyToNull(apiId), emptyToNull(serviceId));
return Flux.fromIterable(apimlList.entrySet())
.map(this::buildEntry)
.onErrorContinue(RuntimeException.class, (ex, consumer) -> log.debug("Unexpected mapping error", ex));
}

private ApimlInfo buildEntry(Map.Entry<String, List<ServiceInfo>> entry) {
return centralApimlInfoMapper.buildApimlServiceInfo(entry.getKey(), entry.getValue());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
@EnableScheduling
@Slf4j
@Component
@ConditionalOnExpression("${apiml.cloudGateway.serviceRegistryEnabled:false}")
@ConditionalOnExpression("${apiml.cloudGateway.registry.enabled:false}")
@RequiredArgsConstructor
public class GatewayScanJob {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/

package org.zowe.apiml.cloudgatewayservice.service;

import lombok.NonNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.zowe.apiml.cloudgatewayservice.service.model.ApimlInfo;
import org.zowe.apiml.cloudgatewayservice.service.model.CentralServiceInfo;
import org.zowe.apiml.config.ApiInfo;
import org.zowe.apiml.services.ServiceInfo;

import java.util.*;
import java.util.stream.Collectors;

import static org.springframework.util.CollectionUtils.isEmpty;


/**
* Build Central Registry Apiml services DTO
*/
@Component
public class CentralApimlInfoMapper {

@Value("${apiml.cloudGateway.registry.metadata-key-allow-list:}")
Set<String> metadataKeysAllowList = new HashSet<>();

public ApimlInfo buildApimlServiceInfo(@NonNull String apimlId, List<ServiceInfo> gatewayServices) {
List<CentralServiceInfo> services = Optional.ofNullable(gatewayServices).orElse(Collections.emptyList()).stream()
.filter(Objects::nonNull)
.map(this::mapServices)
.collect(Collectors.toList());

return ApimlInfo.builder()
.apimlId(apimlId)
.services(services)
.build();
}

private CentralServiceInfo mapServices(ServiceInfo gws) {
return CentralServiceInfo.builder()
.serviceId(gws.getServiceId())
.status(gws.getStatus())
.apiId(extractApiId(gws.getApiml()))
.customMetadata(extractMetadata(gws))
.build();
}

private Map<String, String> extractMetadata(ServiceInfo gws) {
return Optional.ofNullable(gws.getInstances()).orElseGet(Collections::emptyMap)
.entrySet().stream()
.findFirst()
.map(Map.Entry::getValue)
.filter(i -> !isEmpty(i.getCustomMetadata()))
.map(ServiceInfo.Instances::getCustomMetadata)
.orElse(Collections.emptyMap())
.entrySet().stream()
.filter(entry -> metadataKeysAllowList.contains(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

private Set<String> extractApiId(ServiceInfo.Apiml apiml) {
return Optional.ofNullable(apiml)
.map(ServiceInfo.Apiml::getApiInfo)
.orElse(Collections.emptyList()).stream()
.map(ApiInfo::getApiId)
.collect(Collectors.toSet());
}
}
Loading

0 comments on commit ff8ee9b

Please sign in to comment.