Skip to content

Commit

Permalink
Webapp Preview: Authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
koszti authored and wendigo committed Dec 12, 2024
1 parent 4718011 commit b7c86d5
Show file tree
Hide file tree
Showing 28 changed files with 3,009 additions and 1,225 deletions.
26 changes: 26 additions & 0 deletions core/trino-main/src/main/java/io/trino/server/ui/AuthInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.server.ui;

import java.util.Optional;

import static java.util.Objects.requireNonNull;

public record AuthInfo(String authType, boolean passwordAllowed, boolean authenticated, Optional<String> username)
{
public AuthInfo {
requireNonNull(authType, "authType is null");
requireNonNull(username, "username is null");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.server.ui;

import com.google.inject.Inject;
import io.trino.server.security.ResourceSecurity;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import java.util.Optional;

import static io.trino.server.security.ResourceSecurity.AccessType.WEB_UI;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_PREVIEW_AUTH_INFO;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static java.util.Objects.requireNonNull;

@Path(UI_PREVIEW_AUTH_INFO)
@ResourceSecurity(WEB_UI)
public class FixedUserPreviewResource
{
private final String username;

@Inject
public FixedUserPreviewResource(FixedUserWebUiConfig config)
{
this.username = requireNonNull(config, "config is null").getUsername();
}

@GET
@Produces(APPLICATION_JSON)
public AuthInfo getAuthInfo()
{
return new AuthInfo("fixed", false, true, Optional.of(username));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,26 @@ public class FormWebUiAuthenticationFilter
static final String UI_LOGIN = "/ui/login";
static final String UI_LOGOUT = "/ui/logout";

static final String UI_PREVIEW_BASE = "/ui/preview/";

static final String UI_PREVIEW_AUTH_INFO = UI_PREVIEW_BASE + "auth/info";
static final String UI_PREVIEW_LOGIN_FORM = UI_PREVIEW_BASE + "auth/login";
static final String UI_PREVIEW_LOGOUT = UI_PREVIEW_BASE + "auth/logout";

private final JwtParser jwtParser;
private final Function<String, String> jwtGenerator;
private final FormAuthenticator formAuthenticator;
private final Optional<Authenticator> authenticator;

private static final MultipartUiCookie MULTIPART_COOKIE = new MultipartUiCookie(TRINO_UI_COOKIE, "/ui");
private final boolean previewEnabled;

@Inject
public FormWebUiAuthenticationFilter(
FormWebUiConfig config,
FormAuthenticator formAuthenticator,
@ForWebUi Optional<Authenticator> authenticator)
@ForWebUi Optional<Authenticator> authenticator,
WebUiConfig webUiConfig)
{
byte[] hmacBytes;
if (config.getSharedSecret().isPresent()) {
Expand All @@ -97,6 +105,7 @@ public FormWebUiAuthenticationFilter(

this.formAuthenticator = requireNonNull(formAuthenticator, "formAuthenticator is null");
this.authenticator = requireNonNull(authenticator, "authenticator is null");
this.previewEnabled = requireNonNull(webUiConfig, "webUiConfig is null").isPreviewEnabled();
}

@Override
Expand All @@ -116,7 +125,7 @@ public void filter(ContainerRequestContext request)
}

// login and logout resource is not visible to protocol authenticators
if ((path.equals(UI_LOGIN) && request.getMethod().equals("POST")) || path.equals(UI_LOGOUT)) {
if (isLoginResource(path, request.getMethod())) {
return;
}

Expand Down Expand Up @@ -147,6 +156,10 @@ public void filter(ContainerRequestContext request)
return;
}

if (previewEnabled && path.equals(UI_PREVIEW_LOGIN_FORM)) {
return;
}

// redirect to login page
request.abortWith(Response.seeOther(buildLoginFormURI(request)).build());
}
Expand All @@ -171,6 +184,34 @@ private static URI buildLoginFormURI(ContainerRequestContext request)
return builder.build();
}

private boolean isLoginResource(String path, String method)
{
if (path.equals(UI_LOGIN) && method.equals("POST")) {
return true;
}

if (path.equals(UI_LOGOUT)) {
return true;
}

if (!previewEnabled) {
return false;
}

if (path.equals(UI_PREVIEW_LOGIN_FORM) && method.equals("POST")) {
return true;
}

if (path.equals(UI_PREVIEW_LOGOUT)) {
return true;
}

if (path.equals(UI_PREVIEW_AUTH_INFO) && method.equals("GET")) {
return true;
}
return false;
}

private static void handleProtocolLoginRequest(Authenticator authenticator, ContainerRequestContext request)
{
Identity authenticatedIdentity;
Expand Down Expand Up @@ -227,7 +268,7 @@ public Optional<NewCookie[]> checkLoginCredentials(String username, String passw
.map(user -> createAuthenticationCookie(user, secure));
}

private Optional<String> getAuthenticatedUsername(ContainerRequestContext request)
Optional<String> getAuthenticatedUsername(ContainerRequestContext request)
{
try {
return MULTIPART_COOKIE.read(request.getCookies()).map(this::parseJwt);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.server.ui;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.inject.Inject;
import io.trino.server.security.ResourceSecurity;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;

import java.util.Optional;

import static com.google.common.base.Strings.emptyToNull;
import static io.trino.server.security.ResourceSecurity.AccessType.WEB_UI;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_PREVIEW_AUTH_INFO;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_PREVIEW_LOGIN_FORM;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_PREVIEW_LOGOUT;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.getDeleteCookies;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static java.util.Objects.requireNonNull;

@Path("")
@ResourceSecurity(WEB_UI)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public class LoginPreviewResource
{
private final FormWebUiAuthenticationFilter formWebUiAuthenticationManager;

@Inject
public LoginPreviewResource(FormWebUiAuthenticationFilter formWebUiAuthenticationManager)
{
this.formWebUiAuthenticationManager = requireNonNull(formWebUiAuthenticationManager, "formWebUiAuthenticationManager is null");
}

@GET
@Path(UI_PREVIEW_AUTH_INFO)
public AuthInfo getAuthInfo(ContainerRequestContext request, @Context SecurityContext securityContext)
{
boolean isPasswordAllowed = formWebUiAuthenticationManager.isPasswordAllowed(securityContext.isSecure());
Optional<String> username = formWebUiAuthenticationManager.getAuthenticatedUsername(request);
return new AuthInfo("form", isPasswordAllowed, username.isPresent(), username);
}

@POST
@Path(UI_PREVIEW_LOGIN_FORM)
public Response login(LoginForm loginForm, @Context SecurityContext securityContext)
{
String username = emptyToNull(loginForm.username());
String password = emptyToNull(loginForm.password());

if (!formWebUiAuthenticationManager.isAuthenticationEnabled(securityContext.isSecure())) {
throw new ForbiddenException();
}

Optional<NewCookie[]> authenticationCookie = formWebUiAuthenticationManager.checkLoginCredentials(username, password, securityContext.isSecure());
if (authenticationCookie.isEmpty()) {
throw new ForbiddenException();
}

return Response.noContent()
.cookie(authenticationCookie.get())
.build();
}

@GET
@Path(UI_PREVIEW_LOGOUT)
public Response logout(@Context HttpHeaders httpHeaders, @Context SecurityContext securityContext)
{
return Response.noContent()
.cookie(getDeleteCookies(httpHeaders.getCookies(), securityContext.isSecure()))
.build();
}

public record LoginForm(@JsonProperty String username, @JsonProperty String password)
{
@Override
public String username()
{
return username;
}

@Override
public String password()
{
return password;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.trino.server.ui;

import com.google.inject.Inject;
import io.trino.server.ExternalUriInfo;
import io.trino.server.security.ResourceSecurity;
import io.trino.server.security.oauth2.OAuth2Client;
import io.trino.spi.security.Identity;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;

import java.net.URI;
import java.util.Optional;

import static io.trino.server.ServletSecurityUtils.authenticatedIdentity;
import static io.trino.server.security.ResourceSecurity.AccessType.WEB_UI;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_LOGOUT;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_PREVIEW_AUTH_INFO;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.UI_PREVIEW_LOGOUT;
import static io.trino.server.ui.FormWebUiAuthenticationFilter.getDeleteCookies;
import static io.trino.server.ui.OAuthWebUiCookie.delete;
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
import static java.util.Objects.requireNonNull;

@ResourceSecurity(WEB_UI)
public class OAuth2WebUiPreviewResource
{
private final OAuth2Client oAuth2Client;

@Inject
public OAuth2WebUiPreviewResource(OAuth2Client oAuth2Client)
{
this.oAuth2Client = requireNonNull(oAuth2Client, "oAuth2Client is null");
}

@GET
@Path(UI_PREVIEW_AUTH_INFO)
@Produces(APPLICATION_JSON)
public AuthInfo getAuthInfo(ContainerRequestContext request)
{
Optional<String> username = authenticatedIdentity(request).map(Identity::getUser);
return new AuthInfo("oauth2", false, username.isPresent(), username);
}

@GET
@Path(UI_PREVIEW_LOGOUT)
@Produces(APPLICATION_JSON)
public Response logout(@Context HttpHeaders httpHeaders, @BeanParam ExternalUriInfo uriInfo, @Context SecurityContext securityContext)
{
Optional<String> idToken = OAuthIdTokenCookie.read(httpHeaders.getCookies());
URI callbackUri = uriInfo.absolutePath(UI_LOGOUT + "/logout.html");
return Response.seeOther(oAuth2Client.getLogoutEndpoint(idToken, callbackUri)
.orElse(callbackUri))
.cookie(OAuthIdTokenCookie.delete(httpHeaders.getCookies()))
.cookie(getDeleteCookies(httpHeaders.getCookies(), securityContext.isSecure()))
.cookie(delete(httpHeaders.getCookies()))
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ protected void setup(Binder binder)
jaxrsBinder(binder).bind(UiQueryResource.class);

if (buildConfigObject(WebUiConfig.class).isPreviewEnabled()) {
jaxrsBinder(binder).bind(WebUiPreviewStaticResource.class);
install(new WebUiPreviewModule());
}
}
else {
Expand Down
Loading

0 comments on commit b7c86d5

Please sign in to comment.