Skip to content

Commit

Permalink
Feature/query microservices (#18)
Browse files Browse the repository at this point in the history
* Added denied access role filter.

* Updated ProxiedUserDetails to use dynamic type for newInstance method.

* Removed usage of zookeeper stringutils

* Updated with latest changes from integration

* bumped release version

* removed unnecessary accumulo exclusion

* bumped versions for some modules

* Updated with latest changes from main/integration

* Added a server user details supplier bean

* Updated conditionals for server user details supplier

* Implemented authorization and query federation for the query microservices

* Updating federated user logic to match latest changes on integration

* Updated usage of remote user operations for query microservices

* Added ordering to authorization bean registrar

* pr feedback

* PR feedback

* PR Feedback

* pr feedback
  • Loading branch information
jwomeara committed May 20, 2024
1 parent ab244f5 commit 583209a
Show file tree
Hide file tree
Showing 12 changed files with 575 additions and 17 deletions.
24 changes: 10 additions & 14 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
<parent>
<groupId>gov.nsa.datawave.microservice</groupId>
<artifactId>datawave-microservice-service-parent</artifactId>
<version>4.0.4</version>
<version>5.0.0-SNAPSHOT</version>
<relativePath>../../microservice-service-parent/pom.xml</relativePath>
</parent>
<artifactId>spring-boot-starter-datawave</artifactId>
<version>3.0.1-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
<url>https://code.nsa.gov/datawave-spring-boot-starter</url>
<licenses>
<license>
Expand All @@ -26,17 +26,17 @@
<version.accumulo>2.1.1</version.accumulo>
<version.beanutils>1.9.4</version.beanutils>
<version.dropwizard-metrics>4.1.2</version.dropwizard-metrics>
<version.in-memory-accumulo>3.0.1</version.in-memory-accumulo>
<version.in-memory-accumulo>4.0.0-SNAPSHOT</version.in-memory-accumulo>
<version.jackson>2.10.1</version.jackson>
<version.jzlib>1.1.3</version.jzlib>
<version.metrics-spring>3.1.3</version.metrics-spring>
<version.microservice.accumulo-utils>3.0.1</version.microservice.accumulo-utils>
<version.microservice.authorization.api>3.0.0</version.microservice.authorization.api>
<version.microservice.base-rest-responses>3.0.0</version.microservice.base-rest-responses>
<version.microservice.cache-starter>3.0.0</version.microservice.cache-starter>
<version.microservice.common-utils>2.0.0</version.microservice.common-utils>
<version.microservice.metadata-utils>3.0.0</version.microservice.metadata-utils>
<version.microservice.metrics.reporter>2.0.0</version.microservice.metrics.reporter>
<version.microservice.accumulo-utils>4.0.0-SNAPSHOT</version.microservice.accumulo-utils>
<version.microservice.authorization.api>4.0.0-SNAPSHOT</version.microservice.authorization.api>
<version.microservice.base-rest-responses>4.0.0-SNAPSHOT</version.microservice.base-rest-responses>
<version.microservice.cache-starter>4.0.0-SNAPSHOT</version.microservice.cache-starter>
<version.microservice.common-utils>3.0.0-SNAPSHOT</version.microservice.common-utils>
<version.microservice.metadata-utils>4.0.0-SNAPSHOT</version.microservice.metadata-utils>
<version.microservice.metrics.reporter>3.0.0-SNAPSHOT</version.microservice.metrics.reporter>
<version.protostuff>1.6.2</version.protostuff>
<version.springdoc>1.6.9</version.springdoc>
</properties>
Expand Down Expand Up @@ -101,10 +101,6 @@
<artifactId>*</artifactId>
<groupId>junit</groupId>
</exclusion>
<exclusion>
<artifactId>*</artifactId>
<groupId>io.opentelemetry</groupId>
</exclusion>
<!--
NOTE: We have a severe allergy to netty-tcnative and its transitives.
Causes surefire JVMs to core dump in dramatic fashion
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package datawave.microservice.authorization.config;

import java.util.function.Supplier;

import org.springframework.stereotype.Component;

import datawave.user.AuthorizationsListBase;
import datawave.user.DefaultAuthorizationsList;

@Component
public class AuthorizationsListSupplier implements Supplier<AuthorizationsListBase<?>> {
@Override
public AuthorizationsListBase<?> get() {
return new DefaultAuthorizationsList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package datawave.microservice.authorization.federation;

import static org.springframework.beans.factory.config.ConfigurableBeanFactory.SCOPE_PROTOTYPE;

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;

import datawave.microservice.authorization.federation.config.FederatedAuthorizationServiceProperties;

/**
* This class is used to dynamically create and register FederatedAuthorizationService beans via properties.
*/
public class DynamicFederatedAuthorizationServiceBeanDefinitionRegistrar implements BeanDefinitionRegistryPostProcessor, Ordered {

public static final String FEDERATED_AUTHORIZATION_SERVICE_PREFIX = "datawave.authorization.federation.services";
private final Map<String,FederatedAuthorizationServiceProperties> federatedAuthorizationProperties;

public DynamicFederatedAuthorizationServiceBeanDefinitionRegistrar(Environment environment) {
// @formatter:off
federatedAuthorizationProperties = Binder.get(environment)
.bind(FEDERATED_AUTHORIZATION_SERVICE_PREFIX, Bindable.mapOf(String.class, FederatedAuthorizationServiceProperties.class))
.orElse(new HashMap<>());
// @formatter:off
}

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
federatedAuthorizationProperties.forEach((name, props) -> {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
beanDefinition.setBeanClass(FederatedAuthorizationService.class);

ConstructorArgumentValues constructorArgValues = new ConstructorArgumentValues();
constructorArgValues.addGenericArgumentValue(props);
beanDefinition.setConstructorArgumentValues(constructorArgValues);

beanDefinition.setScope(SCOPE_PROTOTYPE);
beanDefinitionRegistry.registerBeanDefinition(name, beanDefinition);
});
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
// intentionally blank
}
@Override
public int getOrder() {
return getPrecedence();
}

public static int getPrecedence() {
return HIGHEST_PRECEDENCE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package datawave.microservice.authorization.federation;

import static datawave.microservice.authorization.preauth.ProxiedEntityX509Filter.ENTITIES_HEADER;
import static datawave.microservice.authorization.preauth.ProxiedEntityX509Filter.ISSUERS_HEADER;

import java.time.Duration;
import java.util.function.Function;

import org.apache.http.HttpHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;

import datawave.microservice.authorization.config.AuthorizationsListSupplier;
import datawave.microservice.authorization.federation.config.FederatedAuthorizationServiceProperties;
import datawave.microservice.authorization.federation.config.FederatedAuthorizationServiceProperties.RetryTimeoutProperties;
import datawave.security.authorization.AuthorizationException;
import datawave.security.authorization.DatawaveUser;
import datawave.security.authorization.ProxiedUserDetails;
import datawave.security.authorization.UserOperations;
import datawave.user.AuthorizationsListBase;
import datawave.webservice.result.GenericResponse;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

public class FederatedAuthorizationService implements UserOperations {
private static final Logger log = LoggerFactory.getLogger(FederatedAuthorizationService.class);

public static final String INCLUDE_REMOTE_SERVICES = "includeRemoteServices";

private FederatedAuthorizationServiceProperties federatedAuthorizationProperties;
private final WebClient webClient;
private AuthorizationsListSupplier authorizationsListSupplier;

public FederatedAuthorizationService(FederatedAuthorizationServiceProperties federatedAuthorizationProperties, WebClient.Builder webClientBuilder,
AuthorizationsListSupplier authorizationsListSupplier) {
this.federatedAuthorizationProperties = federatedAuthorizationProperties;
// @formatter:off
this.webClient = webClientBuilder
.baseUrl(federatedAuthorizationProperties.getFederatedAuthorizationUri())
.exchangeStrategies(ExchangeStrategies.builder()
.codecs(clientCodecConfigurer -> clientCodecConfigurer
.defaultCodecs()
.maxInMemorySize(federatedAuthorizationProperties.getMaxBytesToBuffer()))
.build())
.build();
// @formatter:on
this.authorizationsListSupplier = authorizationsListSupplier;
}

private String getProxiedEntities(ProxiedUserDetails currentUser) {
return getProxiedDN(currentUser, (user) -> user.getDn().subjectDN());
}

private String getProxiedIssuers(ProxiedUserDetails currentUser) {
return getProxiedDN(currentUser, (user) -> user.getDn().issuerDN());
}

private String getProxiedDN(ProxiedUserDetails currentUser, Function<DatawaveUser,String> getDN) {
StringBuilder builder = new StringBuilder();
for (DatawaveUser user : currentUser.getProxiedUsers()) {
builder.append('<').append(getDN.apply(user)).append('>');
}
return builder.toString();
}

@Override
@Cacheable(value = "getRemoteUser", key = "{#currentUser}", cacheManager = "remoteOperationsCacheManager")
public <T extends ProxiedUserDetails> T getRemoteUser(T currentUser) throws AuthorizationException {
return UserOperations.super.getRemoteUser(currentUser);
}

@Override
@Cacheable(value = "listEffectiveAuthorizations", key = "{#currentUser}", cacheManager = "remoteOperationsCacheManager")
public AuthorizationsListBase listEffectiveAuthorizations(ProxiedUserDetails currentUser) throws AuthorizationException {
return listEffectiveAuthorizations(currentUser, true);
}

public AuthorizationsListBase listEffectiveAuthorizations(ProxiedUserDetails currentUser, boolean federate) throws AuthorizationException {
log.info("FederatedAuthorizationService listEffectiveAuthorizations (federate: {}) for {}", federate, currentUser.getPrimaryUser());

try {
// @formatter:off
//noinspection rawtypes,unchecked
ResponseEntity<AuthorizationsListBase> authorizationsListBaseResponseEntity = (ResponseEntity<AuthorizationsListBase>) getResponseEntity(
currentUser,
federate,
federatedAuthorizationProperties.getListEffectiveAuthorizationsRetry(),
"listEffectiveAuthorizations",
authorizationsListSupplier.get().getClass());
// @formatter:on

AuthorizationException authorizationException;
if (authorizationsListBaseResponseEntity != null) {
AuthorizationsListBase authorizationsListBase = authorizationsListBaseResponseEntity.getBody();

if (authorizationsListBaseResponseEntity.getStatusCode() == HttpStatus.OK) {
return authorizationsListBase;
} else {
authorizationException = new AuthorizationException("Unknown error occurred while calling listEffectiveAuthorizations for "
+ currentUser.getPrimaryUser() + ", Status Code: " + authorizationsListBaseResponseEntity.getStatusCodeValue());
}
} else {
authorizationException = new AuthorizationException(
"Unknown error occurred while calling listEffectiveAuthorizations for " + currentUser.getPrimaryUser());
}
throw authorizationException;
} catch (ServiceException e) {
log.error("Timed out waiting for federated listEffectiveAuthorizations response");
throw new AuthorizationException("Timed out waiting for federated listEffectiveAuthorizations response", e);
}
}

@Override
public GenericResponse<String> flushCachedCredentials(ProxiedUserDetails currentUser) throws AuthorizationException {
return flushCachedCredentials(currentUser, true);
}

public GenericResponse<String> flushCachedCredentials(ProxiedUserDetails currentUser, boolean federate) throws AuthorizationException {
log.info("FederatedAuthorizationService flushCachedCredentials (federate: {}) for {}", federate, currentUser.getPrimaryUser());

try {
// @formatter:off
//noinspection rawtypes,unchecked
ResponseEntity<GenericResponse> genericResponseEntity = (ResponseEntity<GenericResponse>)getResponseEntity(
currentUser,
federate,
federatedAuthorizationProperties.getFlushCachedCredentialsRetry(),
"flushCachedCredentials",
GenericResponse.class);
// @formatter:on

AuthorizationException authorizationException;
if (genericResponseEntity != null) {
GenericResponse genericResponse = genericResponseEntity.getBody();

if (genericResponseEntity.getStatusCode() == HttpStatus.OK) {
return genericResponse;
} else {
authorizationException = new AuthorizationException("Unknown error occurred while calling flushCachedCredentials for "
+ currentUser.getPrimaryUser() + ", Status Code: " + genericResponseEntity.getStatusCodeValue());
}
} else {
authorizationException = new AuthorizationException(
"Unknown error occurred while calling flushCachedCredentials for " + currentUser.getPrimaryUser());
}
throw authorizationException;
} catch (ServiceException e) {
log.error("Timed out waiting for federated flushCachedCredentials response");
throw new AuthorizationException("Timed out waiting for federated flushCachedCredentials response", e);
}
}

protected ResponseEntity<?> getResponseEntity(ProxiedUserDetails currentUser, boolean federate, RetryTimeoutProperties retry, String endpoint,
Class entityClass) {
// @formatter:off
return (ResponseEntity<?>) webClient.get()
.uri(uriBuilder -> uriBuilder
.path(endpoint)
.queryParam("includeRemoteServices", federate)
.build())
.header(ENTITIES_HEADER, getProxiedEntities(currentUser))
.header(ISSUERS_HEADER, getProxiedIssuers(currentUser))
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.retrieve()
// don't retry on 4xx errors
.onStatus(HttpStatus::is5xxServerError,
clientResponse -> Mono.error(new ServiceException("Service Error", clientResponse.rawStatusCode())))
.toEntity(entityClass)
.retryWhen(Retry
.fixedDelay(retry.getRetries(), Duration.ofMillis(retry.getRetryDelayMillis()))
.filter(throwable -> throwable instanceof ServiceException)
.onRetryExhaustedThrow(((retryBackoffSpec, retrySignal) -> {
throw new ServiceException("External Service failed to process after max retries",
HttpStatus.SERVICE_UNAVAILABLE.value());
})))
.block(Duration.ofMillis(retry.getTimeoutMillis()));
// @formatter:on
}

public class ServiceException extends RuntimeException {
int statusCode;

public ServiceException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package datawave.microservice.authorization.federation.config;

import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import datawave.security.authorization.UserOperations;

@EnableConfigurationProperties(FederatedAuthorizationProperties.class)
@Configuration
public class FederatedAuthorizationConfiguration {

@Bean
public Set<UserOperations> registeredFederatedUserOperations(FederatedAuthorizationProperties federatedAuthorizationProperties,
Map<String,UserOperations> userOperationsMap) {
Set<UserOperations> registeredFederatedUserOperations = new LinkedHashSet<>();
for (Map.Entry<String,UserOperations> entry : userOperationsMap.entrySet()) {
if (federatedAuthorizationProperties.getRegisteredServices().contains(entry.getKey())) {
registeredFederatedUserOperations.add(entry.getValue());
}
}
return registeredFederatedUserOperations;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package datawave.microservice.authorization.federation.config;

import java.util.LinkedHashSet;
import java.util.Set;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "datawave.authorization.federation")
public class FederatedAuthorizationProperties {
private Set<String> registeredServices = new LinkedHashSet<>();

public Set<String> getRegisteredServices() {
return registeredServices;
}

public void setRegisteredServices(Set<String> registeredServices) {
this.registeredServices = registeredServices;
}
}
Loading

0 comments on commit 583209a

Please sign in to comment.