Skip to content

Commit

Permalink
Add unit tests, rename function
Browse files Browse the repository at this point in the history
  • Loading branch information
Janelle Law committed Nov 29, 2021
1 parent 21cd416 commit 5002c65
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 47 deletions.
97 changes: 51 additions & 46 deletions src/main/java/io/cryostat/net/OpenShiftAuthManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
package io.cryostat.net;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
Expand All @@ -46,6 +47,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -95,8 +97,6 @@ public class OpenShiftAuthManager extends AbstractAuthManager {
private final Function<String, OpenShiftClient> clientProvider;
private final WebClient webClient;

private final CompletableFuture<String> redirectUrl = new CompletableFuture<>();

OpenShiftAuthManager(
Environment env,
Logger logger,
Expand Down Expand Up @@ -297,7 +297,9 @@ public Future<IntermediateResponse<UserInfo>> getOAuthRedirectResponse() {
try {
return CompletableFuture.completedFuture(
new IntermediateResponse<UserInfo>()
.addHeader("X-Location", this.computeAuthorizationEndpoint().get())
.addHeader(
"X-Location",
this.computeAuthorizationEndpoint().get().toString())
.addHeader("access-control-expose-headers", "Location")
.statusCode(302));
} catch (ExecutionException | InterruptedException e) {
Expand Down Expand Up @@ -343,49 +345,52 @@ private Future<TokenReviewStatus> performTokenReview(String token) {
}
}

private Future<String> computeAuthorizationEndpoint()
throws ExecutionException, InterruptedException {

if (redirectUrl.isDone()) {
return redirectUrl;
}

CompletableFuture<JsonObject> oauthMetadata = new CompletableFuture<>();
try {
String clientId =
String.format(
"system:serviceaccount:%s:%s",
this.getNamespace(), env.getEnv("CRYOSTAT_OAUTH_CLIENT_ID"));
String scope =
String.format(
"user:check-access role:%s:%s",
env.getEnv("CRYOSTAT_OAUTH_ROLE"), this.getNamespace());

webClient
.get(443, OAUTH_WELL_KNOWN_HOST, WELL_KNOWN_PATH)
.putHeader("Accept", "application/json")
.send(
ar -> {
if (ar.failed()) {
oauthMetadata.completeExceptionally(ar.cause());
return;
}
oauthMetadata.complete(ar.result().bodyAsJsonObject());
});

String authorizeEndpoint = oauthMetadata.get().getString(OAUTH_ENDPOINT_KEY);

URIBuilder builder = new URIBuilder(authorizeEndpoint);
builder.addParameter("client_id", clientId);
builder.addParameter("response_type", "token");
builder.addParameter("response_mode", "fragment");
builder.addParameter("scope", scope);

redirectUrl.complete(builder.build().toString());
} catch (URISyntaxException | IOException e) {
redirectUrl.completeExceptionally(e);
}
return redirectUrl;
private Future<URI> computeAuthorizationEndpoint() {

return CompletableFuture.supplyAsync(
() -> {
CompletableFuture<JsonObject> oauthMetadata = new CompletableFuture<>();
try {
String namespace = this.getNamespace();
String clientId =
String.format(
"system:serviceaccount:%s:%s",
namespace, env.getEnv("CRYOSTAT_OAUTH_CLIENT_ID"));

String scope =
String.format(
"user:check-access role:%s:%s",
env.getEnv("CRYOSTAT_OAUTH_ROLE"), namespace);

webClient
.get(443, OAUTH_WELL_KNOWN_HOST, WELL_KNOWN_PATH)
.putHeader("Accept", "application/json")
.send(
ar -> {
if (ar.failed()) {
oauthMetadata.completeExceptionally(ar.cause());
return;
}
oauthMetadata.complete(ar.result().bodyAsJsonObject());
});

String authorizeEndpoint =
oauthMetadata.get().getString(OAUTH_ENDPOINT_KEY);

URIBuilder builder = new URIBuilder(authorizeEndpoint);
builder.addParameter("client_id", clientId);
builder.addParameter("response_type", "token");
builder.addParameter("response_mode", "fragment");
builder.addParameter("scope", scope);

return builder.build();
} catch (ExecutionException
| InterruptedException
| URISyntaxException
| IOException e) {
throw new CompletionException(e);
}
});
}

@SuppressFBWarnings(
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/io/cryostat/net/BasicAuthManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ void shouldFailUnknownCredentials() throws Exception {
}

@ParameterizedTest
@ValueSource(strings = {"", "Bearer sometoken", "Basic (not_b64)"})
@ValueSource(strings = {"", "Bearer sometoken", "Basic (not_b64)", "Basic "})
@NullSource
void shouldFailBadCredentials(String s) throws Exception {
Assertions.assertFalse(mgr.validateHttpHeader(() -> s, ResourceAction.NONE).get());
Expand Down
126 changes: 126 additions & 0 deletions src/test/java/io/cryostat/net/OpenShiftAuthManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
Expand All @@ -56,6 +58,7 @@
import io.cryostat.net.security.ResourceAction;
import io.cryostat.net.security.ResourceType;
import io.cryostat.net.security.ResourceVerb;
import io.cryostat.net.web.http.api.v2.IntermediateResponse;

import com.google.gson.Gson;
import io.fabric8.kubernetes.api.model.authentication.TokenReview;
Expand All @@ -67,6 +70,12 @@
import io.fabric8.openshift.client.server.mock.EnableOpenShiftMockClient;
import io.fabric8.openshift.client.server.mock.OpenShiftMockServer;
import io.fabric8.openshift.client.server.mock.OpenShiftMockServerExtension;
import io.vertx.core.AsyncResult;
import io.vertx.core.Handler;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.ext.web.client.HttpResponse;
import io.vertx.ext.web.client.WebClient;
import okhttp3.mockwebserver.RecordedRequest;
import org.apache.commons.lang3.exception.ExceptionUtils;
Expand All @@ -80,10 +89,13 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer;

@ExtendWith({MockitoExtension.class, OpenShiftMockServerExtension.class})
@EnableOpenShiftMockClient(https = false, crud = false)
Expand All @@ -92,6 +104,13 @@ class OpenShiftAuthManagerTest {
static final String SUBJECT_REVIEW_API_PATH =
"/apis/authorization.k8s.io/v1/selfsubjectaccessreviews";
static final String TOKEN_REVIEW_API_PATH = "/apis/authentication.k8s.io/v1/tokenreviews";
static final String AUTHORIZATION_URL = "https://oauth-authorization-url";
static final String OAUTH_CLIENT_ID = "client_id=system%3Aserviceaccount%3Anamespace%3Aoauth-client-id";
static final String OAUTH_TOKEN_PARAMS = "response_type=token&response_mode=fragment";
static final String OAUTH_ROLE_SCOPE = "scope=user%3Acheck-access+role%3Aoauth-role-scope%3Anamespace";
static final String OAUTH_QUERY_PARAMETERS =
String.format("?%s&%s&%s", OAUTH_CLIENT_ID, OAUTH_TOKEN_PARAMS, OAUTH_ROLE_SCOPE);
static final String EXPECTED_REDIRECT_URL = AUTHORIZATION_URL + OAUTH_QUERY_PARAMETERS;

OpenShiftAuthManager mgr;
@Mock Environment env;
Expand All @@ -102,6 +121,7 @@ class OpenShiftAuthManagerTest {
OpenShiftMockServer server;
TokenProvider tokenProvider;
Gson gson = MainModule.provideGson(logger);
@Mock CompletableFuture<String> redirectUrl;

@BeforeAll
static void disableKubeConfig() {
Expand Down Expand Up @@ -264,6 +284,112 @@ void shouldNotValidateTokenWithInsufficientPermissions() throws Exception {
Matchers.equalTo(Paths.get(Config.KUBERNETES_NAMESPACE_PATH)));
}

@ParameterizedTest
@ValueSource(strings = {"", "Bearer ", "invalidHeader"})
void shouldSendRedirectResponseOnEmptyOrInvalidHeaders(String headers) throws Exception {
IntermediateResponse<UserInfo> expectedRedirectResponse =
new IntermediateResponse<UserInfo>()
.addHeader("X-Location", EXPECTED_REDIRECT_URL)
.addHeader("access-control-expose-headers", "Location")
.statusCode(302);
Mockito.when(fs.readFile(Paths.get(Config.KUBERNETES_NAMESPACE_PATH)))
.thenReturn(new BufferedReader(new StringReader("namespace")));
Mockito.when(env.getEnv(Mockito.anyString()))
.thenReturn("oauth-client-id", "oauth-role-scope");
HttpRequest<Buffer> req = Mockito.mock(HttpRequest.class);
HttpResponse<Buffer> resp = Mockito.mock(HttpResponse.class);
Mockito.when(webClient.get(Mockito.anyInt(), Mockito.anyString(), Mockito.anyString()))
.thenReturn(req);
Mockito.when(req.putHeader(Mockito.anyString(), Mockito.anyString())).thenReturn(req);
Mockito.doAnswer(
new Answer<Void>() {
@Override
public Void answer(InvocationOnMock args) throws Throwable {
AsyncResult<HttpResponse<Buffer>> asyncResult =
Mockito.mock(AsyncResult.class);
Mockito.when(asyncResult.result()).thenReturn(resp);
Mockito.when(resp.bodyAsJsonObject())
.thenReturn(
new JsonObject(
Map.of(
"authorization_endpoint",
AUTHORIZATION_URL)));
((Handler<AsyncResult<HttpResponse<Buffer>>>) args.getArgument(0))
.handle(asyncResult);
return null;
}
})
.when(req)
.send(Mockito.any());

IntermediateResponse<UserInfo> actualRedirectResponse =
mgr.sendLoginRedirectIfRequired(() -> headers, ResourceAction.NONE).get();

MatcherAssert.assertThat(
actualRedirectResponse.getHeaders(),
Matchers.equalTo(expectedRedirectResponse.getHeaders()));
MatcherAssert.assertThat(
actualRedirectResponse.getStatusCode(),
Matchers.equalTo(expectedRedirectResponse.getStatusCode()));
MatcherAssert.assertThat(
actualRedirectResponse.getBody(),
Matchers.equalTo(expectedRedirectResponse.getBody()));
}

@ParameterizedTest
@ValueSource(strings = {"Bearer invalidToken", "Bearer 1234"})
void shouldSendRedirectResponseOnInvalidToken(String headers) throws Exception {
IntermediateResponse<UserInfo> expectedRedirectResponse =
new IntermediateResponse<UserInfo>()
.addHeader("X-Location", EXPECTED_REDIRECT_URL)
.addHeader("access-control-expose-headers", "Location")
.statusCode(302);
Mockito.when(fs.readFile(Paths.get(Config.KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH)))
.thenReturn(new BufferedReader(new StringReader("serviceAccountToken")));
Mockito.when(fs.readFile(Paths.get(Config.KUBERNETES_NAMESPACE_PATH)))
.thenReturn(new BufferedReader(new StringReader("namespace")));
Mockito.when(env.getEnv(Mockito.anyString()))
.thenReturn("oauth-client-id", "oauth-role-scope");
HttpRequest<Buffer> req = Mockito.mock(HttpRequest.class);
HttpResponse<Buffer> resp = Mockito.mock(HttpResponse.class);
Mockito.when(webClient.get(Mockito.anyInt(), Mockito.anyString(), Mockito.anyString()))
.thenReturn(req);
Mockito.when(req.putHeader(Mockito.anyString(), Mockito.anyString())).thenReturn(req);
Mockito.doAnswer(
new Answer<Void>() {
@Override
public Void answer(InvocationOnMock args) throws Throwable {
AsyncResult<HttpResponse<Buffer>> asyncResult =
Mockito.mock(AsyncResult.class);
Mockito.when(asyncResult.result()).thenReturn(resp);
Mockito.when(resp.bodyAsJsonObject())
.thenReturn(
new JsonObject(
Map.of(
"authorization_endpoint",
AUTHORIZATION_URL)));
((Handler<AsyncResult<HttpResponse<Buffer>>>) args.getArgument(0))
.handle(asyncResult);
return null;
}
})
.when(req)
.send(Mockito.any());

IntermediateResponse<UserInfo> actualRedirectResponse =
mgr.sendLoginRedirectIfRequired(() -> headers, ResourceAction.NONE).get();

MatcherAssert.assertThat(
actualRedirectResponse.getHeaders(),
Matchers.equalTo(expectedRedirectResponse.getHeaders()));
MatcherAssert.assertThat(
actualRedirectResponse.getStatusCode(),
Matchers.equalTo(expectedRedirectResponse.getStatusCode()));
MatcherAssert.assertThat(
actualRedirectResponse.getBody(),
Matchers.equalTo(expectedRedirectResponse.getBody()));
}

@ParameterizedTest
@EnumSource(
mode = EnumSource.Mode.MATCH_ANY,
Expand Down

0 comments on commit 5002c65

Please sign in to comment.