diff --git a/docs/src/main/asciidoc/amazon-lambda-http.adoc b/docs/src/main/asciidoc/amazon-lambda-http.adoc index 8fcf6ec8e4f9a6..1ab02d8ad79aba 100644 --- a/docs/src/main/asciidoc/amazon-lambda-http.adoc +++ b/docs/src/main/asciidoc/amazon-lambda-http.adoc @@ -276,3 +276,88 @@ public class MyResource { If you are building native images, and want to use https://aws.amazon.com/xray[AWS X-Ray Tracing] with your lambda you will need to include `quarkus-amazon-lambda-xray` as a dependency in your pom. The AWS X-Ray library is not fully compatible with GraalVM so we had to do some integration work to make this work. + +== Security Integration + +When you invoke an HTTP request on the API Gateway, the Gateway turns that HTTP request into a JSON event document that is +forwarded to a Quarkus Lambda. The Quarkus Lambda parses this json and converts in into an internal representation of an HTTP +request that can be consumed by any HTTP framework Quarkus supports (JAX-RS, servlet, Vert.x Web). + +API Gateway supports many different ways to securely invoke on your HTTP endpoints that are backed by Lambda and Quarkus. +By default, Quarkus will automatically parse relevant parts of the https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html[event json document] +and look for security based metadata and register a `java.security.Principal` internally that can be looked up in JAX-RS +by injecting a `javax.ws.rs.core.SecurityContext`, via `HttpServletRequest.getUserPrincipal()` in servlet, and `RouteContext.user()` in Vert.x Web. +If you want more security information, the `Principal` object can be typecast to +a class that will give you more information. + +Here's how its mapped: + +.HTTP `quarkus-amazon-lambda-http` +[options="header"] +|======================= +|Auth Type |Principal Class |Json path of Principal Name +|Cognito JWT |`io.quarkus.amazon.lambda.http.CognitoPrincipal`|`requestContext.authorizer.jwt.claims.cognito:username` +|IAM |`io.quarkus.amazon.lambda.http.IAMPrincipal` |`requestContext.authorizer.iam.userId` +|Custom Lambda |`io.quarkus.amazon.lambda.http.CustomPrincipal` |`requestContext.authorizer.lambda.principalId` + +|======================= + +.REST `quarkus-amazon-lambda-rest` +[options="header"] +|======================= +|Auth Type |Principal Class |Json path of Principal Name +|Cognito |`io.quarkus.amazon.lambda.http.CognitoPrincipal`|`requestContext.authorizer.claims.cognito:username` +|IAM |`io.quarkus.amazon.lambda.http.IAMPrincipal` |`requestContext.identity.user` +|Custom Lambda |`io.quarkus.amazon.lambda.http.CustomPrincipal` |`requestContext.authorizer.principalId` + +|======================= + +== Custom Security Integration + +The default support for AWS security only maps the principal name to Quarkus security +APIs and does nothing to map claims or roles or permissions. You have can full control +how security metadata in the lambda HTTP event is mapped to Quarkus security APIs using +implementations of the `io.quarkus.amazon.lambda.http.LambdaSecurityIdentityProvider` +interface. + +.HTTP `quarkus-amazon-lambda-http` +[source, java] +---- +package io.quarkus.amazon.lambda.http; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.security.identity.SecurityIdentity; + +public interface LambdaSecurityIdentityProvider { + SecurityIdentity create(APIGatewayV2HTTPEvent event); +} +---- + +.REST `quarkus-amazon-lambda-rest` +[source, java] +---- +package io.quarkus.amazon.lambda.http; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.security.identity.SecurityIdentity; + +public interface LambdaSecurityIdentityProvider { + SecurityIdentity create(AwsProxyRequest event); +} +---- + +To plugin an implementation of one of these interfaces, set the +`quarkus.lambda-http.identity-provider` application.properties value +to the fully qualified class name of your implementation. + +When Quarkus receives the HTTP event from the API Gateway, it will invoke the +`create()` method. The `io.quarkus.security.identity.SecurityIdentity` interface +defines how your security metadata maps to standard Quarkus security APIs. In that +implementation, you can define things like role mappings for your principal. + +== Simple SAM Local Principal + +If you are testing your application with `sam local` you can +hardcode a principal name to use when your application runs by setting +the `QUARKUS_AWS_LAMBDA_FORCE_USER_NAME` environment variable diff --git a/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java b/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java index 6f5203ac4bbdbc..4f583ba6b2c45e 100644 --- a/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java +++ b/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java @@ -1,5 +1,7 @@ package io.quarkus.amazon.lambda.http.deployment; +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + import org.jboss.logging.Logger; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; @@ -8,22 +10,32 @@ import io.quarkus.amazon.lambda.deployment.LambdaUtil; import io.quarkus.amazon.lambda.deployment.ProvidedAmazonLambdaHandlerBuildItem; import io.quarkus.amazon.lambda.http.LambdaHttpHandler; +import io.quarkus.amazon.lambda.http.LambdaHttpRecorder; +import io.quarkus.amazon.lambda.http.SecurityIdentityHandler; import io.quarkus.amazon.lambda.http.model.Headers; import io.quarkus.amazon.lambda.http.model.MultiValuedTreeMap; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.runtime.LaunchMode; +import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem; import io.vertx.core.file.impl.FileResolver; public class AmazonLambdaHttpProcessor { private static final Logger log = Logger.getLogger(AmazonLambdaHttpProcessor.class); + @BuildStep + public void addSecurityFilter(BuildProducer filters) { + filters.produce(new FilterBuildItem(new SecurityIdentityHandler(), FilterBuildItem.AUTHENTICATION + 1)); + } + @BuildStep public RequireVirtualHttpBuildItem requestVirtualHttp(LaunchModeBuildItem launchMode) { return launchMode.getLaunchMode() == LaunchMode.NORMAL ? RequireVirtualHttpBuildItem.MARKER : null; @@ -73,4 +85,14 @@ public void generateScripts(OutputTargetBuildItem target, LambdaUtil.writeFile(target, "sam.native.yaml", output); } + @BuildStep() + @Record(STATIC_INIT) + public void setSecurityProvider(LambdaHttpBuildTimeConfig config, + RecorderContext context, + LambdaHttpRecorder recorder) { + if (config.identityProvider.isPresent()) { + recorder.setLambdaSecurityIdentityProvider(context.newInstance(config.identityProvider.get())); + } + } + } diff --git a/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java b/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java new file mode 100644 index 00000000000000..501394193ea57d --- /dev/null +++ b/extensions/amazon-lambda-http/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java @@ -0,0 +1,13 @@ +package io.quarkus.amazon.lambda.http.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot +public class LambdaHttpBuildTimeConfig { + /** + * Fully qualified class name of custom io.quarkus.lambda.http.LambdaSecurityIdentityProvider + */ + public Optional identityProvider; +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java new file mode 100644 index 00000000000000..32dea02ac75a43 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java @@ -0,0 +1,30 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +/** + * Represents a Cognito JWT used to authenticate request + * + * Will only be allocated if requestContext.authorizer.jwt.claims.cognito:username is set + * in the http event sent by API Gateway + */ +public class CognitoPrincipal implements Principal { + private APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT jwt; + private String name; + + public CognitoPrincipal(APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT jwt) { + this.jwt = jwt; + this.name = jwt.getClaims().get("cognito:username"); + } + + @Override + public String getName() { + return name; + } + + public APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT getClaims() { + return jwt; + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java new file mode 100644 index 00000000000000..cbeb661cde8a1b --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java @@ -0,0 +1,30 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; +import java.util.Map; + +/** + * Represents a custom principal sent by API Gateway i.e. a Lambda authorizer + * + * Will only be allocated if requestContext.authorizer.lambda.principalId is set + * in the http event sent by API Gateway + * + */ +public class CustomPrincipal implements Principal { + private String name; + private Map claims; + + public CustomPrincipal(String name, Map claims) { + this.claims = claims; + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public Map getClaims() { + return claims; + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaSecurityIdentityProvider.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaSecurityIdentityProvider.java new file mode 100644 index 00000000000000..52a8275fc165b3 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaSecurityIdentityProvider.java @@ -0,0 +1,63 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; +import java.util.Map; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.security.identity.SecurityIdentity; + +public class DefaultLambdaSecurityIdentityProvider implements LambdaSecurityIdentityProvider { + @Override + public SecurityIdentity create(APIGatewayV2HTTPEvent event) { + Principal principal = getPrincipal(event); + if (principal != null) { + return new LambdaSecurityIdentity(principal); + } + return null; + } + + protected Principal getPrincipal(APIGatewayV2HTTPEvent request) { + final Map systemEnvironment = System.getenv(); + final boolean isSamLocal = Boolean.parseBoolean(systemEnvironment.get("AWS_SAM_LOCAL")); + final APIGatewayV2HTTPEvent.RequestContext requestContext = request.getRequestContext(); + if (isSamLocal && (requestContext == null || requestContext.getAuthorizer() == null)) { + final String forcedUserName = systemEnvironment.get("QUARKUS_AWS_LAMBDA_FORCE_USER_NAME"); + if (forcedUserName != null && !forcedUserName.isEmpty()) { + return new Principal() { + + @Override + public String getName() { + return forcedUserName; + } + + }; + } + } else { + if (requestContext != null) { + final APIGatewayV2HTTPEvent.RequestContext.Authorizer authorizer = requestContext.getAuthorizer(); + if (authorizer != null) { + if (authorizer.getJwt() != null) { + final APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT jwt = authorizer.getJwt(); + final Map claims = jwt.getClaims(); + if (claims != null && claims.containsKey("cognito:username")) { + return new CognitoPrincipal(jwt); + } + } else if (authorizer.getIam() != null) { + if (authorizer.getIam().getUserId() != null) { + return new IAMPrincipal(authorizer.getIam()); + } + } else if (authorizer.getLambda() != null) { + Object tmp = authorizer.getLambda().get("principalId"); + if (tmp != null && tmp instanceof String) { + String username = (String) tmp; + return new CustomPrincipal(username, authorizer.getLambda()); + } + } + } + } + } + return null; + } + +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java new file mode 100644 index 00000000000000..a236dbcca98b13 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java @@ -0,0 +1,30 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +/** + * Used if IAM is used for authentication. + * + * Will only be allocated if requestContext.authorizer.iam.userId is set + * in the http event sent by API Gateway + */ +public class IAMPrincipal implements Principal { + private String name; + private APIGatewayV2HTTPEvent.RequestContext.IAM iam; + + public IAMPrincipal(APIGatewayV2HTTPEvent.RequestContext.IAM iam) { + this.iam = iam; + this.name = iam.getUserId(); + } + + @Override + public String getName() { + return name; + } + + public APIGatewayV2HTTPEvent.RequestContext.IAM getIam() { + return iam; + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java index 90187615ed3e80..a3f4af729afde8 100644 --- a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java @@ -39,6 +39,7 @@ import io.quarkus.amazon.lambda.http.model.Headers; import io.quarkus.netty.runtime.virtual.VirtualClientConnection; import io.quarkus.netty.runtime.virtual.VirtualResponseHandler; +import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.http.runtime.QuarkusHttpHeaders; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; @@ -169,9 +170,9 @@ private APIGatewayV2HTTPResponse nettyDispatch(InetSocketAddress clientAddress, quarkusHeaders.setContextObject(Context.class, context); quarkusHeaders.setContextObject(APIGatewayV2HTTPEvent.class, request); quarkusHeaders.setContextObject(APIGatewayV2HTTPEvent.RequestContext.class, request.getRequestContext()); - final Principal principal = getPrincipal(request); - if (principal != null) { - quarkusHeaders.setContextObject(Principal.class, principal); + final SecurityIdentity identity = LambdaHttpRecorder.identityProvider.create(request); + if (identity != null) { + quarkusHeaders.setContextObject(SecurityIdentity.class, identity); } DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(request.getRequestContext().getHttp().getMethod()), ofNullable(request.getRawQueryString()) @@ -242,7 +243,8 @@ private boolean isBinary(String contentType) { private Principal getPrincipal(APIGatewayV2HTTPEvent request) { final Map systemEnvironment = System.getenv(); final boolean isSamLocal = Boolean.parseBoolean(systemEnvironment.get("AWS_SAM_LOCAL")); - if (isSamLocal) { + final APIGatewayV2HTTPEvent.RequestContext requestContext = request.getRequestContext(); + if (isSamLocal && (requestContext == null || requestContext.getAuthorizer() == null)) { final String forcedUserName = systemEnvironment.get("QUARKUS_AWS_LAMBDA_FORCE_USER_NAME"); if (forcedUserName != null && !forcedUserName.isEmpty()) { log.info("Forcing local user to " + forcedUserName); @@ -256,22 +258,24 @@ public String getName() { }; } } else { - final APIGatewayV2HTTPEvent.RequestContext requestContext = request.getRequestContext(); if (requestContext != null) { final APIGatewayV2HTTPEvent.RequestContext.Authorizer authorizer = requestContext.getAuthorizer(); if (authorizer != null) { - final APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT jwt = authorizer.getJwt(); - if (jwt != null) { + if (authorizer.getJwt() != null) { + final APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT jwt = authorizer.getJwt(); final Map claims = jwt.getClaims(); - if (claims != null) { - final String jwtUsername = claims.get("cognito:username"); - if (jwtUsername != null && !jwtUsername.isEmpty()) - return new Principal() { - @Override - public String getName() { - return jwtUsername; - } - }; + if (claims != null && claims.containsKey("cognito:username")) { + return new CognitoPrincipal(jwt); + } + } else if (authorizer.getIam() != null) { + if (authorizer.getIam().getUserId() != null) { + return new IAMPrincipal(authorizer.getIam()); + } + } else if (authorizer.getLambda() != null) { + Object tmp = authorizer.getLambda().get("principalId"); + if (tmp != null && tmp instanceof String) { + String username = (String) tmp; + return new CustomPrincipal(username, authorizer.getLambda()); } } } diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpRecorder.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpRecorder.java new file mode 100644 index 00000000000000..2dca18128a9dec --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpRecorder.java @@ -0,0 +1,13 @@ +package io.quarkus.amazon.lambda.http; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class LambdaHttpRecorder { + static LambdaSecurityIdentityProvider identityProvider = new DefaultLambdaSecurityIdentityProvider(); + + public void setLambdaSecurityIdentityProvider(RuntimeValue provider) { + identityProvider = provider.getValue(); + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentity.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentity.java new file mode 100644 index 00000000000000..b13193c9845a08 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentity.java @@ -0,0 +1,64 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Permission; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.quarkus.security.credential.Credential; +import io.quarkus.security.identity.SecurityIdentity; +import io.smallrye.mutiny.Uni; + +public class LambdaSecurityIdentity implements SecurityIdentity { + protected Principal principal; + + public LambdaSecurityIdentity(Principal principal) { + this.principal = principal; + } + + @Override + public Principal getPrincipal() { + return principal; + } + + @Override + public boolean isAnonymous() { + return false; + } + + @Override + public Set getRoles() { + return Collections.emptySet(); + } + + @Override + public boolean hasRole(String role) { + return false; + } + + @Override + public T getCredential(Class credentialType) { + return null; + } + + @Override + public Set getCredentials() { + return Collections.emptySet(); + } + + @Override + public T getAttribute(String name) { + return null; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } + + @Override + public Uni checkPermission(Permission permission) { + return Uni.createFrom().item(false); + } +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentityProvider.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentityProvider.java new file mode 100644 index 00000000000000..84ffeeabfdf7f7 --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentityProvider.java @@ -0,0 +1,9 @@ +package io.quarkus.amazon.lambda.http; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.security.identity.SecurityIdentity; + +public interface LambdaSecurityIdentityProvider { + SecurityIdentity create(APIGatewayV2HTTPEvent event); +} diff --git a/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/SecurityIdentityHandler.java b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/SecurityIdentityHandler.java new file mode 100644 index 00000000000000..01fa3f656e0b6a --- /dev/null +++ b/extensions/amazon-lambda-http/runtime/src/main/java/io/quarkus/amazon/lambda/http/SecurityIdentityHandler.java @@ -0,0 +1,31 @@ +package io.quarkus.amazon.lambda.http; + +import java.util.Map; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.QuarkusHttpHeaders; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.ext.web.RoutingContext; + +/** + * Filter that will check to see if LambdaHttpHandler forwarded a SecurityIdentity. + * If so, it sets the RoutingContext.user + * + */ +public class SecurityIdentityHandler implements Handler { + @Override + public void handle(RoutingContext routingContext) { + MultiMap qheaders = routingContext.request().headers(); + if (qheaders instanceof QuarkusHttpHeaders) { + Map, Object> contextObjects = ((QuarkusHttpHeaders) qheaders).getContextObjects(); + if (contextObjects.containsKey(SecurityIdentity.class)) { + SecurityIdentity identity = (SecurityIdentity) contextObjects.get(SecurityIdentity.class); + routingContext.setUser(new QuarkusHttpUser(identity)); + } + } + routingContext.next(); + + } +} diff --git a/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java index ab26b36b472d6c..a2d1c8eecdbc14 100644 --- a/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java +++ b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/AmazonLambdaHttpProcessor.java @@ -1,10 +1,14 @@ package io.quarkus.amazon.lambda.http.deployment; +import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; + import org.jboss.logging.Logger; import io.quarkus.amazon.lambda.deployment.LambdaUtil; import io.quarkus.amazon.lambda.deployment.ProvidedAmazonLambdaHandlerBuildItem; import io.quarkus.amazon.lambda.http.LambdaHttpHandler; +import io.quarkus.amazon.lambda.http.LambdaHttpRecorder; +import io.quarkus.amazon.lambda.http.SecurityIdentityHandler; import io.quarkus.amazon.lambda.http.model.AlbContext; import io.quarkus.amazon.lambda.http.model.ApiGatewayAuthorizerContext; import io.quarkus.amazon.lambda.http.model.ApiGatewayRequestIdentity; @@ -17,18 +21,26 @@ import io.quarkus.amazon.lambda.http.model.MultiValuedTreeMap; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.LaunchModeBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem; +import io.quarkus.deployment.recording.RecorderContext; import io.quarkus.runtime.LaunchMode; +import io.quarkus.vertx.http.deployment.FilterBuildItem; import io.quarkus.vertx.http.deployment.RequireVirtualHttpBuildItem; import io.vertx.core.file.impl.FileResolver; public class AmazonLambdaHttpProcessor { private static final Logger log = Logger.getLogger(AmazonLambdaHttpProcessor.class); + @BuildStep + public void addSecurityFilter(BuildProducer filters) { + filters.produce(new FilterBuildItem(new SecurityIdentityHandler(), FilterBuildItem.AUTHENTICATION + 1)); + } + @BuildStep public RequireVirtualHttpBuildItem requestVirtualHttp(LaunchModeBuildItem launchMode) { return launchMode.getLaunchMode() == LaunchMode.NORMAL ? RequireVirtualHttpBuildItem.MARKER : null; @@ -80,4 +92,14 @@ public void generateScripts(OutputTargetBuildItem target, LambdaUtil.writeFile(target, "sam.native.yaml", output); } + @BuildStep() + @Record(STATIC_INIT) + public void setSecurityProvider(LambdaHttpBuildTimeConfig config, + RecorderContext context, + LambdaHttpRecorder recorder) { + if (config.identityProvider.isPresent()) { + recorder.setLambdaSecurityIdentityProvider(context.newInstance(config.identityProvider.get())); + } + } + } diff --git a/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java new file mode 100644 index 00000000000000..501394193ea57d --- /dev/null +++ b/extensions/amazon-lambda-rest/deployment/src/main/java/io/quarkus/amazon/lambda/http/deployment/LambdaHttpBuildTimeConfig.java @@ -0,0 +1,13 @@ +package io.quarkus.amazon.lambda.http.deployment; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot +public class LambdaHttpBuildTimeConfig { + /** + * Fully qualified class name of custom io.quarkus.lambda.http.LambdaSecurityIdentityProvider + */ + public Optional identityProvider; +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java new file mode 100644 index 00000000000000..d3a22e2ad52f8c --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CognitoPrincipal.java @@ -0,0 +1,31 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; + +import io.quarkus.amazon.lambda.http.model.CognitoAuthorizerClaims; + +/** + * Allocated when cognito is used to authenticate user + * + * Will only be allocated if requestContext.authorizer.claims.cognito:username is set + * in the http event sent by API Gateway + * + */ +public class CognitoPrincipal implements Principal { + private CognitoAuthorizerClaims claims; + private String name; + + public CognitoPrincipal(CognitoAuthorizerClaims claims) { + this.claims = claims; + this.name = claims.getUsername(); + } + + @Override + public String getName() { + return name; + } + + public CognitoAuthorizerClaims getClaims() { + return claims; + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java new file mode 100644 index 00000000000000..1f5bdf8f5a8a3d --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/CustomPrincipal.java @@ -0,0 +1,30 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; +import java.util.Map; + +/** + * Allocated when a custom authorizer (i.e. Lambda) is used to authenticate user + * + * Will only be allocated if requestContext.authorizer.principalId is set + * in the http event sent by API Gateway + * + */ +public class CustomPrincipal implements Principal { + private String name; + private Map claims; + + public CustomPrincipal(String name, Map claims) { + this.claims = claims; + this.name = name; + } + + @Override + public String getName() { + return name; + } + + public Map getClaims() { + return claims; + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaSecurityIdentityProvider.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaSecurityIdentityProvider.java new file mode 100644 index 00000000000000..baf115251515f8 --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/DefaultLambdaSecurityIdentityProvider.java @@ -0,0 +1,54 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; +import java.util.Map; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.amazon.lambda.http.model.AwsProxyRequestContext; +import io.quarkus.security.identity.SecurityIdentity; + +public class DefaultLambdaSecurityIdentityProvider implements LambdaSecurityIdentityProvider { + @Override + public SecurityIdentity create(AwsProxyRequest event) { + Principal principal = getPrincipal(event); + if (principal != null) { + return new LambdaSecurityIdentity(principal); + } + return null; + } + + private Principal getPrincipal(AwsProxyRequest request) { + final Map systemEnvironment = System.getenv(); + final boolean isSamLocal = Boolean.parseBoolean(systemEnvironment.get("AWS_SAM_LOCAL")); + final AwsProxyRequestContext requestContext = request.getRequestContext(); + if (isSamLocal && (requestContext == null + || (requestContext.getAuthorizer() == null && requestContext.getIdentity() == null))) { + final String forcedUserName = systemEnvironment.get("QUARKUS_AWS_LAMBDA_FORCE_USER_NAME"); + if (forcedUserName != null && !forcedUserName.isEmpty()) { + return new Principal() { + + @Override + public String getName() { + return forcedUserName; + } + + }; + } + } else { + if (requestContext != null) { + if (requestContext.getIdentity() != null && requestContext.getIdentity().getUser() != null) { + return new IAMPrincipal(requestContext.getIdentity()); + } else if (requestContext.getAuthorizer() != null) { + if (requestContext.getAuthorizer().getClaims() != null) { + return new CognitoPrincipal(requestContext.getAuthorizer().getClaims()); + } else if (requestContext.getAuthorizer().getPrincipalId() != null) { + return new CustomPrincipal(requestContext.getAuthorizer().getPrincipalId(), + requestContext.getAuthorizer().getContextProperties()); + } + } + } + } + return null; + } + +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java new file mode 100644 index 00000000000000..61c88ebb43d1b9 --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/IAMPrincipal.java @@ -0,0 +1,31 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Principal; + +import io.quarkus.amazon.lambda.http.model.ApiGatewayRequestIdentity; + +/** + * Allocated when IAM is used to authenticate user + * + * Will only be allocated if requestContext.identity.user is set + * in the http event sent by API Gateway + * + */ +public class IAMPrincipal implements Principal { + private String name; + private ApiGatewayRequestIdentity iam; + + public IAMPrincipal(ApiGatewayRequestIdentity identity) { + this.iam = identity; + this.name = identity.getUser(); + } + + @Override + public String getName() { + return name; + } + + public ApiGatewayRequestIdentity getIam() { + return iam; + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java index 484264e245e4de..d474a31bb702d2 100644 --- a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpHandler.java @@ -35,6 +35,7 @@ import io.quarkus.amazon.lambda.http.model.Headers; import io.quarkus.netty.runtime.virtual.VirtualClientConnection; import io.quarkus.netty.runtime.virtual.VirtualResponseHandler; +import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.vertx.http.runtime.QuarkusHttpHeaders; import io.quarkus.vertx.http.runtime.VertxHttpRecorder; @@ -178,6 +179,10 @@ private AwsProxyResponse nettyDispatch(InetSocketAddress clientAddress, AwsProxy QuarkusHttpHeaders quarkusHeaders = new QuarkusHttpHeaders(); quarkusHeaders.setContextObject(Context.class, context); quarkusHeaders.setContextObject(AwsProxyRequestContext.class, request.getRequestContext()); + final SecurityIdentity identity = LambdaHttpRecorder.identityProvider.create(request); + if (identity != null) { + quarkusHeaders.setContextObject(SecurityIdentity.class, identity); + } DefaultHttpRequest nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(request.getHttpMethod()), path, quarkusHeaders); if (request.getMultiValueHeaders() != null) { //apparently this can be null if no headers are sent diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpRecorder.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpRecorder.java new file mode 100644 index 00000000000000..2dca18128a9dec --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaHttpRecorder.java @@ -0,0 +1,13 @@ +package io.quarkus.amazon.lambda.http; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class LambdaHttpRecorder { + static LambdaSecurityIdentityProvider identityProvider = new DefaultLambdaSecurityIdentityProvider(); + + public void setLambdaSecurityIdentityProvider(RuntimeValue provider) { + identityProvider = provider.getValue(); + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentity.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentity.java new file mode 100644 index 00000000000000..b13193c9845a08 --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentity.java @@ -0,0 +1,64 @@ +package io.quarkus.amazon.lambda.http; + +import java.security.Permission; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.quarkus.security.credential.Credential; +import io.quarkus.security.identity.SecurityIdentity; +import io.smallrye.mutiny.Uni; + +public class LambdaSecurityIdentity implements SecurityIdentity { + protected Principal principal; + + public LambdaSecurityIdentity(Principal principal) { + this.principal = principal; + } + + @Override + public Principal getPrincipal() { + return principal; + } + + @Override + public boolean isAnonymous() { + return false; + } + + @Override + public Set getRoles() { + return Collections.emptySet(); + } + + @Override + public boolean hasRole(String role) { + return false; + } + + @Override + public T getCredential(Class credentialType) { + return null; + } + + @Override + public Set getCredentials() { + return Collections.emptySet(); + } + + @Override + public T getAttribute(String name) { + return null; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } + + @Override + public Uni checkPermission(Permission permission) { + return Uni.createFrom().item(false); + } +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentityProvider.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentityProvider.java new file mode 100644 index 00000000000000..fdc6574808bfcd --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/LambdaSecurityIdentityProvider.java @@ -0,0 +1,8 @@ +package io.quarkus.amazon.lambda.http; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.security.identity.SecurityIdentity; + +public interface LambdaSecurityIdentityProvider { + SecurityIdentity create(AwsProxyRequest event); +} diff --git a/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/SecurityIdentityHandler.java b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/SecurityIdentityHandler.java new file mode 100644 index 00000000000000..01fa3f656e0b6a --- /dev/null +++ b/extensions/amazon-lambda-rest/runtime/src/main/java/io/quarkus/amazon/lambda/http/SecurityIdentityHandler.java @@ -0,0 +1,31 @@ +package io.quarkus.amazon.lambda.http; + +import java.util.Map; + +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.vertx.http.runtime.QuarkusHttpHeaders; +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.ext.web.RoutingContext; + +/** + * Filter that will check to see if LambdaHttpHandler forwarded a SecurityIdentity. + * If so, it sets the RoutingContext.user + * + */ +public class SecurityIdentityHandler implements Handler { + @Override + public void handle(RoutingContext routingContext) { + MultiMap qheaders = routingContext.request().headers(); + if (qheaders instanceof QuarkusHttpHeaders) { + Map, Object> contextObjects = ((QuarkusHttpHeaders) qheaders).getContextObjects(); + if (contextObjects.containsKey(SecurityIdentity.class)) { + SecurityIdentity identity = (SecurityIdentity) contextObjects.get(SecurityIdentity.class); + routingContext.setUser(new QuarkusHttpUser(identity)); + } + } + routingContext.next(); + + } +} diff --git a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java index dc09f4c24777ad..aa6532a2f53f3b 100644 --- a/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java +++ b/extensions/undertow/runtime/src/main/java/io/quarkus/undertow/runtime/UndertowDeploymentRecorder.java @@ -223,7 +223,9 @@ public RuntimeValue createDeployment(String name, Set kn public void handleNotification(SecurityNotification notification) { if (notification.getEventType() == SecurityNotification.EventType.AUTHENTICATED) { QuarkusUndertowAccount account = (QuarkusUndertowAccount) notification.getAccount(); - CDI.current().select(CurrentIdentityAssociation.class).get().setIdentity(account.getSecurityIdentity()); + Instance instance = CDI.current().select(CurrentIdentityAssociation.class); + if (instance.isResolvable()) + instance.get().setIdentity(account.getSecurityIdentity()); } } }); diff --git a/integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java b/integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java new file mode 100644 index 00000000000000..73ebc7aa50f56f --- /dev/null +++ b/integration-tests/amazon-lambda-http-resteasy/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java @@ -0,0 +1,18 @@ +package io.quarkus.it.amazon.lambda; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +@Path("security") +public class SecurityCheckResource { + + @GET + @Produces("text/plain") + @Path("username") + public String getUsername(@Context SecurityContext ctx) { + return ctx.getUserPrincipal().getName(); + } +} diff --git a/integration-tests/amazon-lambda-http-resteasy/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java b/integration-tests/amazon-lambda-http-resteasy/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java index 03f4a55cd48b25..59f12ce224b031 100644 --- a/integration-tests/amazon-lambda-http-resteasy/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java +++ b/integration-tests/amazon-lambda-http-resteasy/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java @@ -17,6 +17,18 @@ @QuarkusTest public class AmazonLambdaSimpleTestCase { + @Test + public void testCognitoJWTSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/security/username"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setJwt(new APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT()); + request.getRequestContext().getAuthorizer().getJwt().setClaims(new HashMap<>()); + request.getRequestContext().getAuthorizer().getJwt().getClaims().put("cognito:username", "Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + @Test public void testContext() throws Exception { APIGatewayV2HTTPEvent request = new APIGatewayV2HTTPEvent(); diff --git a/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/CustomSecurityProvider.java b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/CustomSecurityProvider.java new file mode 100644 index 00000000000000..4a54ad8251d8f3 --- /dev/null +++ b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/CustomSecurityProvider.java @@ -0,0 +1,74 @@ +package io.quarkus.it.amazon.lambda; + +import java.security.Permission; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; + +import io.quarkus.amazon.lambda.http.LambdaSecurityIdentityProvider; +import io.quarkus.security.credential.Credential; +import io.quarkus.security.identity.SecurityIdentity; +import io.smallrye.mutiny.Uni; + +public class CustomSecurityProvider implements LambdaSecurityIdentityProvider { + @Override + public SecurityIdentity create(APIGatewayV2HTTPEvent event) { + if (event.getHeaders() == null || !event.getHeaders().containsKey("x-user")) + return null; + + return new SecurityIdentity() { + @Override + public Principal getPrincipal() { + return new Principal() { + @Override + public String getName() { + return event.getHeaders().get("x-user"); + } + }; + } + + @Override + public boolean isAnonymous() { + return false; + } + + @Override + public Set getRoles() { + return Collections.EMPTY_SET; + } + + @Override + public boolean hasRole(String role) { + return false; + } + + @Override + public T getCredential(Class credentialType) { + return null; + } + + @Override + public Set getCredentials() { + return null; + } + + @Override + public T getAttribute(String name) { + return null; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } + + @Override + public Uni checkPermission(Permission permission) { + return Uni.createFrom().item(false); + } + }; + } +} diff --git a/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java new file mode 100644 index 00000000000000..73ebc7aa50f56f --- /dev/null +++ b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckResource.java @@ -0,0 +1,18 @@ +package io.quarkus.it.amazon.lambda; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +@Path("security") +public class SecurityCheckResource { + + @GET + @Produces("text/plain") + @Path("username") + public String getUsername(@Context SecurityContext ctx) { + return ctx.getUserPrincipal().getName(); + } +} diff --git a/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckVertx.java b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckVertx.java new file mode 100644 index 00000000000000..3828fc58af6fb9 --- /dev/null +++ b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityCheckVertx.java @@ -0,0 +1,16 @@ +package io.quarkus.it.amazon.lambda; + +import static io.quarkus.vertx.web.Route.HttpMethod.GET; + +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.quarkus.vertx.web.Route; +import io.vertx.ext.web.RoutingContext; + +public class SecurityCheckVertx { + @Route(path = "/vertx/security", methods = GET) + void hello(RoutingContext context) { + context.response().headers().set("Content-Type", "text/plain"); + context.response().setStatusCode(200) + .end(((QuarkusHttpUser) context.user()).getSecurityIdentity().getPrincipal().getName()); + } +} diff --git a/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityServlet.java b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityServlet.java new file mode 100644 index 00000000000000..8926090982cbdb --- /dev/null +++ b/integration-tests/amazon-lambda-http/src/main/java/io/quarkus/it/amazon/lambda/SecurityServlet.java @@ -0,0 +1,19 @@ +package io.quarkus.it.amazon.lambda; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet(name = "ServletSecurity", urlPatterns = "/servlet/security") +public class SecurityServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(200); + resp.addHeader("Content-Type", "text/plain"); + resp.getWriter().write(req.getUserPrincipal().getName()); + } +} diff --git a/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java b/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java index afebdd73b5ddb9..1bf83981f24d71 100644 --- a/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java +++ b/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaSimpleTestCase.java @@ -30,6 +30,64 @@ public void testContext() throws Exception { Assertions.assertEquals(out.getStatusCode(), 204); } + @Test + public void testJaxrsCognitoJWTSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/security/username"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setJwt(new APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT()); + request.getRequestContext().getAuthorizer().getJwt().setClaims(new HashMap<>()); + request.getRequestContext().getAuthorizer().getJwt().getClaims().put("cognito:username", "Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + + @Test + public void testJaxrsIAMSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/security/username"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setIam(new APIGatewayV2HTTPEvent.RequestContext.IAM()); + request.getRequestContext().getAuthorizer().getIam().setUserId("Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + + @Test + public void testJaxrsCustomLambdaSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/security/username"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setLambda(new HashMap<>()); + request.getRequestContext().getAuthorizer().getLambda().put("principalId", "Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + + @Test + public void testServletCognitoJWTSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/servlet/security"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setJwt(new APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT()); + request.getRequestContext().getAuthorizer().getJwt().setClaims(new HashMap<>()); + request.getRequestContext().getAuthorizer().getJwt().getClaims().put("cognito:username", "Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + + @Test + public void testVertxCognitoJWTSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/vertx/security"); + request.getRequestContext().setAuthorizer(new APIGatewayV2HTTPEvent.RequestContext.Authorizer()); + request.getRequestContext().getAuthorizer().setJwt(new APIGatewayV2HTTPEvent.RequestContext.Authorizer.JWT()); + request.getRequestContext().getAuthorizer().getJwt().setClaims(new HashMap<>()); + request.getRequestContext().getAuthorizer().getJwt().getClaims().put("cognito:username", "Bill"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "Bill"); + } + @Test public void testGetText() throws Exception { testGetText("/vertx/hello"); diff --git a/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderIT.java b/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderIT.java new file mode 100644 index 00000000000000..2e00eb3c77e614 --- /dev/null +++ b/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.amazon.lambda; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class CustomSecurityProviderIT extends CustomSecurityProviderTestCase { +} diff --git a/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderProfile.java b/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderProfile.java new file mode 100644 index 00000000000000..aebaa03a63a0b4 --- /dev/null +++ b/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderProfile.java @@ -0,0 +1,13 @@ +package io.quarkus.it.amazon.lambda; + +import java.util.Collections; +import java.util.Map; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class CustomSecurityProviderProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Collections.singletonMap("quarkus.lambda-http.identity-provider", CustomSecurityProvider.class.getName()); + } +} diff --git a/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderTestCase.java b/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderTestCase.java new file mode 100644 index 00000000000000..3e638a0be93a44 --- /dev/null +++ b/integration-tests/amazon-lambda-http/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderTestCase.java @@ -0,0 +1,43 @@ +package io.quarkus.it.amazon.lambda; + +import java.util.HashMap; + +import org.apache.commons.codec.binary.Base64; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; + +import io.quarkus.amazon.lambda.test.LambdaClient; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(CustomSecurityProviderProfile.class) +public class CustomSecurityProviderTestCase { + @Test + public void testJaxrsCognitoJWTSecurityContext() throws Exception { + APIGatewayV2HTTPEvent request = request("/security/username"); + request.getHeaders().put("x-user", "John"); + APIGatewayV2HTTPResponse out = LambdaClient.invoke(APIGatewayV2HTTPResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertEquals(body(out), "John"); + } + + private APIGatewayV2HTTPEvent request(String path) { + APIGatewayV2HTTPEvent request = new APIGatewayV2HTTPEvent(); + request.setHeaders(new HashMap<>()); + request.setRawPath(path); + request.setRequestContext(new APIGatewayV2HTTPEvent.RequestContext()); + request.getRequestContext().setHttp(new APIGatewayV2HTTPEvent.RequestContext.Http()); + request.getRequestContext().getHttp().setMethod("GET"); + return request; + } + + private String body(APIGatewayV2HTTPResponse response) { + if (!response.getIsBase64Encoded()) + return response.getBody(); + return new String(Base64.decodeBase64(response.getBody())); + } +} diff --git a/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/CustomSecurityProvider.java b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/CustomSecurityProvider.java new file mode 100644 index 00000000000000..845dea1564ca73 --- /dev/null +++ b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/CustomSecurityProvider.java @@ -0,0 +1,73 @@ +package io.quarkus.it.amazon.lambda.v1; + +import java.security.Permission; +import java.security.Principal; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.quarkus.amazon.lambda.http.LambdaSecurityIdentityProvider; +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.security.credential.Credential; +import io.quarkus.security.identity.SecurityIdentity; +import io.smallrye.mutiny.Uni; + +public class CustomSecurityProvider implements LambdaSecurityIdentityProvider { + @Override + public SecurityIdentity create(AwsProxyRequest event) { + if (event.getMultiValueHeaders() == null || !event.getMultiValueHeaders().containsKey("x-user")) + return null; + + return new SecurityIdentity() { + @Override + public Principal getPrincipal() { + return new Principal() { + @Override + public String getName() { + return event.getMultiValueHeaders().getFirst("x-user"); + } + }; + } + + @Override + public boolean isAnonymous() { + return false; + } + + @Override + public Set getRoles() { + return Collections.EMPTY_SET; + } + + @Override + public boolean hasRole(String role) { + return false; + } + + @Override + public T getCredential(Class credentialType) { + return null; + } + + @Override + public Set getCredentials() { + return null; + } + + @Override + public T getAttribute(String name) { + return null; + } + + @Override + public Map getAttributes() { + return Collections.emptyMap(); + } + + @Override + public Uni checkPermission(Permission permission) { + return Uni.createFrom().item(false); + } + }; + } +} diff --git a/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckResource.java b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckResource.java new file mode 100644 index 00000000000000..32df6f74888485 --- /dev/null +++ b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckResource.java @@ -0,0 +1,18 @@ +package io.quarkus.it.amazon.lambda.v1; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +@Path("security") +public class SecurityCheckResource { + + @GET + @Produces("text/plain") + @Path("username") + public String getUsername(@Context SecurityContext ctx) { + return ctx.getUserPrincipal().getName(); + } +} diff --git a/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckVertx.java b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckVertx.java new file mode 100644 index 00000000000000..61e7a7226eb905 --- /dev/null +++ b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityCheckVertx.java @@ -0,0 +1,16 @@ +package io.quarkus.it.amazon.lambda.v1; + +import static io.quarkus.vertx.web.Route.HttpMethod.GET; + +import io.quarkus.vertx.http.runtime.security.QuarkusHttpUser; +import io.quarkus.vertx.web.Route; +import io.vertx.ext.web.RoutingContext; + +public class SecurityCheckVertx { + @Route(path = "/vertx/security", methods = GET) + void hello(RoutingContext context) { + context.response().headers().set("Content-Type", "text/plain"); + context.response().setStatusCode(200) + .end(((QuarkusHttpUser) context.user()).getSecurityIdentity().getPrincipal().getName()); + } +} diff --git a/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityServlet.java b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityServlet.java new file mode 100644 index 00000000000000..06b27d15045433 --- /dev/null +++ b/integration-tests/amazon-lambda-rest/src/main/java/io/quarkus/it/amazon/lambda/v1/SecurityServlet.java @@ -0,0 +1,19 @@ +package io.quarkus.it.amazon.lambda.v1; + +import java.io.IOException; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@WebServlet(name = "ServletSecurity", urlPatterns = "/servlet/security") +public class SecurityServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setStatus(200); + resp.addHeader("Content-Type", "text/plain"); + resp.getWriter().write(req.getUserPrincipal().getName()); + } +} diff --git a/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaV1SimpleTestCase.java b/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaV1SimpleTestCase.java index e99974e08bad40..b16ceefce0751b 100644 --- a/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaV1SimpleTestCase.java +++ b/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/AmazonLambdaV1SimpleTestCase.java @@ -8,9 +8,12 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import io.quarkus.amazon.lambda.http.model.ApiGatewayAuthorizerContext; +import io.quarkus.amazon.lambda.http.model.ApiGatewayRequestIdentity; import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; import io.quarkus.amazon.lambda.http.model.AwsProxyRequestContext; import io.quarkus.amazon.lambda.http.model.AwsProxyResponse; +import io.quarkus.amazon.lambda.http.model.CognitoAuthorizerClaims; import io.quarkus.amazon.lambda.http.model.Headers; import io.quarkus.amazon.lambda.test.LambdaClient; import io.quarkus.test.junit.QuarkusTest; @@ -27,6 +30,59 @@ public void testContext() throws Exception { Assertions.assertEquals(out.getStatusCode(), 204); } + @Test + public void testJaxrsSecurityIAM() throws Exception { + AwsProxyRequest request = new AwsProxyRequest(); + request.setHttpMethod("GET"); + request.setPath("/security/username"); + request.setRequestContext(new AwsProxyRequestContext()); + request.getRequestContext().setIdentity(new ApiGatewayRequestIdentity()); + request.getRequestContext().getIdentity().setUser("Bill"); + AwsProxyResponse out = LambdaClient.invoke(AwsProxyResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertTrue(body(out).contains("Bill")); + } + + @Test + public void testServletSecurityIAM() throws Exception { + AwsProxyRequest request = new AwsProxyRequest(); + request.setHttpMethod("GET"); + request.setPath("/servlet/security"); + request.setRequestContext(new AwsProxyRequestContext()); + request.getRequestContext().setIdentity(new ApiGatewayRequestIdentity()); + request.getRequestContext().getIdentity().setUser("Bill"); + AwsProxyResponse out = LambdaClient.invoke(AwsProxyResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertTrue(body(out).contains("Bill")); + } + + @Test + public void testJaxrsCognitoSecurityContext() throws Exception { + AwsProxyRequest request = new AwsProxyRequest(); + request.setHttpMethod("GET"); + request.setPath("/security/username"); + request.setRequestContext(new AwsProxyRequestContext()); + request.getRequestContext().setAuthorizer(new ApiGatewayAuthorizerContext()); + request.getRequestContext().getAuthorizer().setClaims(new CognitoAuthorizerClaims()); + request.getRequestContext().getAuthorizer().getClaims().setUsername("Bill"); + AwsProxyResponse out = LambdaClient.invoke(AwsProxyResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertTrue(body(out).contains("Bill")); + } + + @Test + public void testJaxrsCustomLambdaSecurityContext() throws Exception { + AwsProxyRequest request = new AwsProxyRequest(); + request.setHttpMethod("GET"); + request.setPath("/security/username"); + request.setRequestContext(new AwsProxyRequestContext()); + request.getRequestContext().setAuthorizer(new ApiGatewayAuthorizerContext()); + request.getRequestContext().getAuthorizer().setPrincipalId("Bill"); + AwsProxyResponse out = LambdaClient.invoke(AwsProxyResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertTrue(body(out).contains("Bill")); + } + @Test public void testGetText() throws Exception { testGetText("/vertx/hello"); diff --git a/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderIT.java b/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderIT.java new file mode 100644 index 00000000000000..2e00eb3c77e614 --- /dev/null +++ b/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.amazon.lambda; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +public class CustomSecurityProviderIT extends CustomSecurityProviderTestCase { +} diff --git a/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderProfile.java b/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderProfile.java new file mode 100644 index 00000000000000..22afea60c86f51 --- /dev/null +++ b/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderProfile.java @@ -0,0 +1,14 @@ +package io.quarkus.it.amazon.lambda; + +import java.util.Collections; +import java.util.Map; + +import io.quarkus.it.amazon.lambda.v1.CustomSecurityProvider; +import io.quarkus.test.junit.QuarkusTestProfile; + +public class CustomSecurityProviderProfile implements QuarkusTestProfile { + @Override + public Map getConfigOverrides() { + return Collections.singletonMap("quarkus.lambda-http.identity-provider", CustomSecurityProvider.class.getName()); + } +} diff --git a/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderTestCase.java b/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderTestCase.java new file mode 100644 index 00000000000000..fc37609af243e7 --- /dev/null +++ b/integration-tests/amazon-lambda-rest/src/test/java/io/quarkus/it/amazon/lambda/CustomSecurityProviderTestCase.java @@ -0,0 +1,34 @@ +package io.quarkus.it.amazon.lambda; + +import org.apache.commons.codec.binary.Base64; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.amazon.lambda.http.model.AwsProxyRequest; +import io.quarkus.amazon.lambda.http.model.AwsProxyResponse; +import io.quarkus.amazon.lambda.http.model.Headers; +import io.quarkus.amazon.lambda.test.LambdaClient; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.TestProfile; + +@QuarkusTest +@TestProfile(CustomSecurityProviderProfile.class) +public class CustomSecurityProviderTestCase { + @Test + public void testJaxrsCognitoJWTSecurityContext() throws Exception { + AwsProxyRequest request = new AwsProxyRequest(); + request.setMultiValueHeaders(new Headers()); + request.getMultiValueHeaders().add("x-user", "John"); + request.setHttpMethod("GET"); + request.setPath("/security/username"); + AwsProxyResponse out = LambdaClient.invoke(AwsProxyResponse.class, request); + Assertions.assertEquals(out.getStatusCode(), 200); + Assertions.assertTrue(body(out).contains("John")); + } + + private String body(AwsProxyResponse response) { + if (!response.isBase64Encoded()) + return response.getBody(); + return new String(Base64.decodeBase64(response.getBody())); + } +}