Skip to content

Commit

Permalink
Webapp Preview: Authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
koszti committed Nov 19, 2024
1 parent 2125ef9 commit 946bca0
Show file tree
Hide file tree
Showing 27 changed files with 2,980 additions and 1,222 deletions.
30 changes: 30 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,30 @@
/*
* 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 isPasswordAllowed,
boolean isAuthenticated,
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,51 @@
/*
* 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 jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

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("")
public class FixedUserPreviewResource
{
private final FixedUserWebUiConfig fixedUserWebUiConfig;

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

@ResourceSecurity(WEB_UI)
@GET
@Path(UI_PREVIEW_AUTH_INFO)
@Produces(APPLICATION_JSON)
public AuthInfo getAuthInfo(ContainerRequestContext request, @Context SecurityContext securityContext)
{
return new AuthInfo("fixed", false, true, Optional.of(fixedUserWebUiConfig.getUsername()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public class FormWebUiAuthenticationFilter
public static final String UI_LOCATION = "/ui/";
static final String UI_LOGIN = "/ui/login";
static final String UI_LOGOUT = "/ui/logout";
static final String UI_PREVIEW_LOGIN_FORM = "/ui/preview/";
static final String UI_PREVIEW_AUTH_INFO = "/ui/preview/auth/info";
static final String UI_PREVIEW_LOGIN = "/ui/preview/auth/login";
static final String UI_PREVIEW_LOGOUT = "/ui/preview/auth/logout";

private final JwtParser jwtParser;
private final Function<String, String> jwtGenerator;
Expand Down Expand Up @@ -116,7 +120,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 All @@ -143,7 +147,7 @@ public void filter(ContainerRequestContext request)
return;
}

if (path.equals(LOGIN_FORM)) {
if (path.equals(LOGIN_FORM) || path.equals(UI_PREVIEW_LOGIN)) {
return;
}

Expand Down Expand Up @@ -171,6 +175,26 @@ private static URI buildLoginFormURI(ContainerRequestContext request)
return builder.build();
}

private static boolean isLoginResource(String path, String method)
{
if (path.equals(UI_LOGIN) && method.equals("POST")) {
return true;
}
if (path.equals(UI_PREVIEW_LOGIN) && method.equals("POST")) {
return true;
}
if (path.equals(UI_LOGOUT) || path.equals(UI_PREVIEW_LOGOUT)) {
return true;
}
if (path.equals(UI_PREVIEW_LOGIN_FORM) && method.equals("GET")) {
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 +251,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,116 @@
/*
* 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;
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("")
public class LoginPreviewResource
{
private final FormWebUiAuthenticationFilter formWebUiAuthenticationManager;

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

@ResourceSecurity(WEB_UI)
@GET
@Path(UI_PREVIEW_AUTH_INFO)
@Produces(APPLICATION_JSON)
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);
}

@ResourceSecurity(WEB_UI)
@POST
@Path(UI_PREVIEW_LOGIN)
@Consumes(APPLICATION_JSON)
@Produces(APPLICATION_JSON)
public Response login(LoginForm loginForm, @Context SecurityContext securityContext)
{
final String username = emptyToNull(loginForm.username);
final 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();
}

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

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

@Override
@JsonProperty
public String password()
{
return password;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* 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 io.trino.server.security.oauth2.OAuth2Client;
import io.trino.spi.security.Identity;
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 jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.core.UriInfo;

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;

@Path("")
public class OAuth2WebUiPreviewResource
{
private final OAuth2Client oAuth2Client;

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

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

@ResourceSecurity(WEB_UI)
@GET
@Path(UI_PREVIEW_LOGOUT)
@Produces(APPLICATION_JSON)
public Response logout(@Context HttpHeaders httpHeaders, @Context UriInfo uriInfo, @Context SecurityContext securityContext)
{
Optional<String> idToken = OAuthIdTokenCookie.read(httpHeaders.getCookies());
URI callBackUri = UriBuilder.fromUri(uriInfo.getAbsolutePath())
.replacePath(UI_LOGOUT + "/logout.html")
.build();
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();
}
}
Loading

0 comments on commit 946bca0

Please sign in to comment.