Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

b/260769399 Allow injection of multiple notification backends #202

Merged
merged 1 commit into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import com.google.solutions.jitaccess.core.services.RoleDiscoveryService;

import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Context;
Expand Down Expand Up @@ -68,7 +69,7 @@ public class ApiResource {
ActivationTokenService activationTokenService;

@Inject
NotificationService notificationService;
Instance<NotificationService> notificationServices;

@Inject
RuntimeEnvironment runtimeEnvironment;
Expand Down Expand Up @@ -389,7 +390,7 @@ public ActivationStatusResponse requestActivation(
) throws AccessDeniedException {
Preconditions.checkNotNull(this.roleDiscoveryService, "roleDiscoveryService");
assert this.activationTokenService != null;
assert this.notificationService != null;
assert this.notificationServices != null;

var minReviewers = this.roleActivationService.getOptions().minNumberOfReviewersPerActivationRequest;
var maxReviewers = this.roleActivationService.getOptions().maxNumberOfReviewersPerActivationRequest;
Expand All @@ -414,8 +415,14 @@ public ActivationStatusResponse requestActivation(
request.justification != null && request.justification.length() < 100,
"The justification is too long");

//
// For MPA to work, we need at least one functional notification service.
//
Preconditions.checkState(
this.notificationService.canSendNotifications() || this.runtimeEnvironment.isDebugModeEnabled(),
this.notificationServices
.stream()
.anyMatch(s -> s.canSendNotifications()) ||
this.runtimeEnvironment.isDebugModeEnabled(),
"The multi-party approval feature is not available because the server-side configuration is incomplete");

var iapPrincipal = (UserPrincipal) securityContext.getUserPrincipal();
Expand All @@ -441,10 +448,13 @@ public ActivationStatusResponse requestActivation(
// Create an approval token and pass it to reviewers.
//
var activationToken = this.activationTokenService.createToken(activationRequest);
this.notificationService.sendNotification(new RequestActivationNotification(
activationRequest,
activationToken.expiryTime,
createActivationRequestUrl(uriInfo, activationToken.token)));

for (var service : this.notificationServices) {
service.sendNotification(new RequestActivationNotification(
activationRequest,
activationToken.expiryTime,
createActivationRequestUrl(uriInfo, activationToken.token)));
}

this.logAdapter
.newInfoEntry(
Expand Down Expand Up @@ -549,7 +559,7 @@ public ActivationStatusResponse approveActivationRequest(
) throws AccessException {
assert this.activationTokenService != null;
assert this.roleActivationService != null;
assert this.notificationService != null;
assert this.notificationServices != null;

Preconditions.checkArgument(
obfuscatedActivationToken != null && !obfuscatedActivationToken.trim().isEmpty(),
Expand Down Expand Up @@ -580,10 +590,12 @@ public ActivationStatusResponse approveActivationRequest(

assert activation != null;

this.notificationService.sendNotification(new ActivationApprovedNotification(
activationRequest,
iapPrincipal.getId(),
createActivationRequestUrl(uriInfo, activationToken)));
for (var service : this.notificationServices) {
service.sendNotification(new ActivationApprovedNotification(
activationRequest,
iapPrincipal.getId(),
createActivationRequestUrl(uriInfo, activationToken)));
}

this.logAdapter
.newInfoEntry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@
import com.google.solutions.jitaccess.core.ApplicationVersion;
import com.google.solutions.jitaccess.core.adapters.*;
import com.google.solutions.jitaccess.core.data.UserId;
import com.google.solutions.jitaccess.core.services.ActivationTokenService;
import com.google.solutions.jitaccess.core.services.NotificationService;
import com.google.solutions.jitaccess.core.services.RoleActivationService;
import com.google.solutions.jitaccess.core.services.RoleDiscoveryService;
import com.google.solutions.jitaccess.core.services.*;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
Expand Down Expand Up @@ -291,7 +288,7 @@ public ActivationTokenService.Options getTokenServiceOptions() {

@Produces
@ApplicationScoped
public NotificationService getNotificationService(
public NotificationService getEmailNotificationService(
SecretManagerAdapter secretManagerAdapter
) {
//
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.google.solutions.jitaccess.core.data.RoleBinding;
import com.google.solutions.jitaccess.core.data.UserId;
import com.google.solutions.jitaccess.core.services.*;
import jakarta.enterprise.inject.Instance;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
Expand Down Expand Up @@ -63,6 +64,7 @@ public class TestApiResource {
new ActivationTokenService.TokenWithExpiry(SAMPLE_TOKEN, Instant.now().plusSeconds(10));

private ApiResource resource;
private NotificationService notificationService;

@BeforeEach
public void before() {
Expand All @@ -73,9 +75,14 @@ public void before() {
this.resource.roleDiscoveryService = Mockito.mock(RoleDiscoveryService.class);
this.resource.roleActivationService = Mockito.mock(RoleActivationService.class);
this.resource.activationTokenService = Mockito.mock(ActivationTokenService.class);
this.resource.notificationService = Mockito.mock(NotificationService.class);

when(this.resource.notificationService.canSendNotifications()).thenReturn(true);
this.notificationService = Mockito.mock(NotificationService.class);
when(this.notificationService.canSendNotifications()).thenReturn(true);

this.resource.notificationServices = Mockito.mock(Instance.class);
when(this.resource.notificationServices.stream()).thenReturn(List.of(notificationService).stream());
when(this.resource.notificationServices.iterator()).thenReturn(List.of(notificationService).iterator());

when(this.resource.runtimeEnvironment.createAbsoluteUriBuilder(any(UriInfo.class)))
.thenReturn(UriBuilder.fromUri("https://localhost/"));
}
Expand Down Expand Up @@ -744,8 +751,7 @@ public void whenNotificationsNotConfigured_ThenRequestActivationReturnsError() t
DEFAULT_MIN_NUMBER_OF_REVIEWERS,
DEFAULT_MAX_NUMBER_OF_REVIEWERS));

this.resource.notificationService = Mockito.mock(NotificationService.class);
when(this.resource.notificationService.canSendNotifications()).thenReturn(false);
when(this.notificationService.canSendNotifications()).thenReturn(false);

var request = new ApiResource.ActivationRequest();
request.role = "roles/mock";
Expand Down Expand Up @@ -842,7 +848,7 @@ public void whenRequestValid_ThenRequestActivationSendsNotification() throws Exc
ApiResource.ActivationStatusResponse.class);
assertEquals(200, response.getStatus());

verify(this.resource.notificationService, times(1))
verify(this.notificationService, times(1))
.sendNotification(argThat(n -> n instanceof ApiResource.RequestActivationNotification));
}

Expand Down Expand Up @@ -1084,7 +1090,7 @@ public void whenTokenValid_ThenApproveActivationSendsNotification() throws Excep

assertEquals(200, response.getStatus());

verify(this.resource.notificationService, times(1))
verify(this.notificationService, times(1))
.sendNotification(argThat(n -> n instanceof ApiResource.ActivationApprovedNotification));
}

Expand Down