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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX-5017 Add authorization metrics #5074

Merged
merged 11 commits into from
Mar 28, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static java.util.Objects.requireNonNull;

import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import org.slf4j.Logger;
Expand All @@ -30,9 +31,13 @@
import com.linecorp.armeria.common.annotation.Nullable;
import com.linecorp.armeria.common.util.Exceptions;
import com.linecorp.armeria.server.HttpService;
import com.linecorp.armeria.server.ServiceConfig;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.SimpleDecoratingHttpService;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;

/**
* Decorates an {@link HttpService} to provide HTTP authorization functionality.
*
Expand Down Expand Up @@ -75,6 +80,7 @@ public static AuthServiceBuilder builder() {
private final Authorizer<HttpRequest> authorizer;
private final AuthSuccessHandler defaultSuccessHandler;
private final AuthFailureHandler defaultFailureHandler;
private Timer timer;

AuthService(HttpService delegate, Authorizer<HttpRequest> authorizer,
AuthSuccessHandler defaultSuccessHandler, AuthFailureHandler defaultFailureHandler) {
Expand All @@ -84,8 +90,18 @@ public static AuthServiceBuilder builder() {
this.defaultFailureHandler = defaultFailureHandler;
}

@Override
public void serviceAdded(ServiceConfig cfg) throws Exception {
super.serviceAdded(cfg);
final MeterRegistry meterRegistry = cfg.server().meterRegistry();
timer = Timer.builder("armeria.server.auth")
yashmurty marked this conversation as resolved.
Show resolved Hide resolved
.register(meterRegistry);
}

@Override
public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exception {
final long startNanos = System.nanoTime();

return HttpResponse.of(AuthorizerUtil.authorizeAndSupplyHandlers(authorizer, ctx, req)
.handleAsync((result, cause) -> {
try {
Expand All @@ -104,6 +120,10 @@ public HttpResponse serve(ServiceRequestContext ctx, HttpRequest req) throws Exc
ctx, req, cause);
} catch (Exception e) {
return Exceptions.throwUnsafely(e);
} finally {
// Record the time taken to authorize the request
final long endNanos = System.nanoTime();
timer.record(endNanos - startNanos, TimeUnit.NANOSECONDS);
}
}, ctx.eventLoop()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static com.linecorp.armeria.common.util.UnmodifiableFuture.completedFuture;
import static org.assertj.core.api.Assertions.assertThat;

import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Base64.Encoder;
Expand Down Expand Up @@ -52,6 +53,7 @@
import com.linecorp.armeria.common.auth.BasicToken;
import com.linecorp.armeria.common.auth.OAuth1aToken;
import com.linecorp.armeria.common.auth.OAuth2Token;
import com.linecorp.armeria.common.metric.MoreMeters;
import com.linecorp.armeria.common.util.UnmodifiableFuture;
import com.linecorp.armeria.internal.testing.AnticipatedException;
import com.linecorp.armeria.server.AbstractHttpService;
Expand All @@ -61,6 +63,9 @@
import com.linecorp.armeria.server.logging.LoggingService;
import com.linecorp.armeria.testing.junit5.server.ServerExtension;

import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import io.netty.util.AsciiString;

class AuthServiceTest {
Expand Down Expand Up @@ -95,7 +100,7 @@ protected HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) {
() -> "unit test".equals(req.headers().get(AUTHORIZATION)));
sb.service(
"/",
ok.decorate(AuthService.newDecorator(authorizer))
ok.decorate(AuthService.builder().add(authorizer).newDecorator())
.decorate(LoggingService.newDecorator()));

// Auth with HTTP basic
Expand Down Expand Up @@ -170,8 +175,8 @@ protected HttpResponse doGet(ServiceRequestContext ctx, HttpRequest req) {
sb.service(
"/authorizer_exception",
ok.decorate(AuthService.builder().add((ctx, data) -> {
throw new AnticipatedException("bug!");
}).newDecorator())
throw new AnticipatedException("bug!");
}).newDecorator())
.decorate(LoggingService.newDecorator()));

// Authorizer returns a future that resolves to null.
Expand Down Expand Up @@ -429,11 +434,23 @@ void shouldPeelRedundantAuthorizerExceptions() throws Exception {
.onFailure((delegate, ctx, req, cause) -> {
causeRef.set(cause);
return HttpResponse.of(HttpStatus.FORBIDDEN);
}).build((ctx, req) -> HttpResponse.of("OK"));
})
.build((ctx, req) -> HttpResponse.of("OK"));

// Create a meter registry and set a timer to the service.
final MeterRegistry meterRegistry = new SimpleMeterRegistry();
final Field timerField = AuthService.class.getDeclaredField("timer");
timerField.setAccessible(true);
jrhee17 marked this conversation as resolved.
Show resolved Hide resolved
final Timer timer = Timer.builder("armeria.server.auth").register(meterRegistry);
timerField.set(service, timer);
assertThat(MoreMeters.measureAll(meterRegistry)).containsEntry("armeria.server.auth#count", 0.0);

final ServiceRequestContext ctx = ServiceRequestContext.of(HttpRequest.of(HttpMethod.GET, "/"));
final HttpResponse response = service.serve(ctx, ctx.request());

assertThat(response.aggregate().join().status()).isEqualTo(HttpStatus.FORBIDDEN);
assertThat(causeRef.get()).isInstanceOf(AnticipatedException.class);
assertThat(MoreMeters.measureAll(meterRegistry)).containsEntry("armeria.server.auth#count", 1.0);
}

private static HttpUriRequestBase getRequest(String path, String authorization) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,31 +19,21 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import com.linecorp.armeria.client.BlockingWebClient;
import com.linecorp.armeria.client.grpc.GrpcClients;
import com.linecorp.armeria.common.AggregatedHttpResponse;
import com.linecorp.armeria.common.HttpRequest;
import com.linecorp.armeria.common.HttpStatus;
import com.linecorp.armeria.common.util.UnmodifiableFuture;
import com.linecorp.armeria.internal.common.JacksonUtil;
import com.linecorp.armeria.server.HttpService;
import com.linecorp.armeria.server.ServerBuilder;
import com.linecorp.armeria.server.ServiceRequestContext;
import com.linecorp.armeria.server.annotation.DecoratorFactory;
import com.linecorp.armeria.server.annotation.DecoratorFactoryFunction;
import com.linecorp.armeria.server.auth.AuthService;
import com.linecorp.armeria.server.auth.Authorizer;
import com.linecorp.armeria.server.grpc.GrpcService;
Expand All @@ -66,41 +56,45 @@ public class GrpcHttpJsonTranscodingServiceAnnotatedAuthServiceTest {
@Override
protected void configure(ServerBuilder sb) throws Exception {
final GrpcService grpcService = GrpcService.builder()
.addService(new AuthenticatedHttpJsonTranscodingTestService())
.enableHttpJsonTranscoding(true)
.build();
.addService(new AuthenticatedHttpJsonTranscodingTestService())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind reverting incorrect indentations in this file?

.enableHttpJsonTranscoding(true)
.build();

sb.requestTimeoutMillis(5000);
sb.decorator(LoggingService.newDecorator());
sb.service(grpcService);
sb.serviceUnder("/",
grpcService.decorate(
AuthService.builder()
.add(new TestAuthorizer())
.newDecorator()));
}
};

private static final String TEST_CREDENTIAL_KEY = "credential";

private final ObjectMapper mapper = JacksonUtil.newDefaultObjectMapper();

private final BlockingWebClient webClient = server.webClient().blocking();

private final HttpJsonTranscodingTestServiceBlockingStub grpcClient =
GrpcClients.newClient(server.httpUri(), HttpJsonTranscodingTestServiceBlockingStub.class);

@Test
void testAuthenticatedRpcMethod() throws Exception {
final Transcoding.GetMessageRequestV1 requestMessage = Transcoding.GetMessageRequestV1.newBuilder()
final Transcoding.GetMessageRequestV1 requestMessage = Transcoding.GetMessageRequestV1
.newBuilder()
.setName("messages/1").build();
final Throwable exception = assertThrows(Throwable.class,
() -> grpcClient.getMessageV1(requestMessage).getText());
() -> grpcClient.getMessageV1(requestMessage).getText());
assertThat(exception).isInstanceOf(StatusRuntimeException.class);
assertThat(((StatusRuntimeException) exception).getStatus().getCode())
.isEqualTo(Status.UNAUTHENTICATED.getCode());

final Metadata metadata = new Metadata();
metadata.put(Metadata.Key.of(TEST_CREDENTIAL_KEY, Metadata.ASCII_STRING_MARSHALLER),
"some-credential-string");
"some-credential-string");
final Transcoding.Message result =
grpcClient.withInterceptors(
MetadataUtils.newAttachHeadersInterceptor(metadata)
).getMessageV1(requestMessage);
MetadataUtils.newAttachHeadersInterceptor(metadata)
).getMessageV1(requestMessage);
assertThat(result.getText()).isEqualTo("messages/1");
}

Expand All @@ -110,38 +104,24 @@ void testAuthenticatedHttpJsonTranscoding() throws Exception {
assertThat(failResponse.status()).isEqualTo(HttpStatus.UNAUTHORIZED);

final JsonNode root = webClient.prepare()
.get("/v1/messages/1")
.header(TEST_CREDENTIAL_KEY, "some-credential-string")
.asJson(JsonNode.class)
.execute()
.content();
.get("/v1/messages/1")
.header(TEST_CREDENTIAL_KEY, "some-credential-string")
.asJson(JsonNode.class)
.execute()
.content();
assertThat(root.get("text").asText()).isEqualTo("messages/1");
}

private static class AuthenticatedHttpJsonTranscodingTestService
extends HttpJsonTranscodingTestServiceImplBase {
@Override
@Authenticate
public void getMessageV1(Transcoding.GetMessageRequestV1 request,
StreamObserver<Transcoding.Message> responseObserver) {
responseObserver.onNext(Transcoding.Message.newBuilder().setText(request.getName()).build());
responseObserver.onCompleted();
}
}

@DecoratorFactory(AuthServiceDecoratorFactoryFunction.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
private @interface Authenticate {}

private static class AuthServiceDecoratorFactoryFunction implements DecoratorFactoryFunction<Authenticate> {
@Override
public Function<? super HttpService, ? extends HttpService>
newDecorator(Authenticate parameter) {
return AuthService.newDecorator(new TestAuthorizer());
}
}

private static class TestAuthorizer implements Authorizer<HttpRequest> {
@Override
public CompletionStage<Boolean> authorize(ServiceRequestContext ctx, HttpRequest req) {
Expand Down