Skip to content

Commit

Permalink
Implement Flight Auth1 and Auth2
Browse files Browse the repository at this point in the history
  • Loading branch information
nbauernfeind committed Sep 2, 2022
1 parent c895513 commit 343e103
Show file tree
Hide file tree
Showing 39 changed files with 1,341 additions and 511 deletions.
53 changes: 0 additions & 53 deletions Util/src/main/java/io/deephaven/util/auth/AuthContext.java

This file was deleted.

9 changes: 9 additions & 0 deletions authentication/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Flight's gRPC call for Handshake() takes a HandshakeRequest, which ostensibly provides both a payload in bytes and a
protocol version int64, but the protocol version value is never written by current FlightClient implementations, leaving
servers to only recognize the authentication details by the payload bytes.

For the provided Flight.BasicAuth payload, no indicator is included that the payload _is_ a basic username/password pair
to authenticate with, so Deephaven prefers a specific envelope, both to confirm that the payload is the envelope, and
also to provide a specific type to expect the nested payload to be. This does mean that for typed payloads, there will
be three levels of nesting - the HandshakeRequest will contain a Deephaven TypedAuthenticationPayload, which will then
contain the actual provided details.
13 changes: 13 additions & 0 deletions authentication/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
plugins {
id 'java-library'
id 'io.deephaven.project.register'
}

description 'authentication: Deephaven authentication and identity'

dependencies {
api project(':Base')
api project(':proto:proto-backplane-grpc')

Classpaths.inheritArrow(project, 'flight-core', 'implementation')
}
1 change: 1 addition & 0 deletions authentication/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.deephaven.project.ProjectType=JAVA_PUBLIC
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.deephaven.auth;

import java.nio.ByteBuffer;
import java.util.Optional;

/**
* Handler to accept an empty payload and accept that user as anonymous. To prevent anonymous access, do not enable this
* authentication handler.
*/
public class AnonymousAuthenticationHandler implements AuthenticationRequestHandler {
@Override
public String getAuthType() {
return "Anonymous";
}

@Override
public Optional<AuthContext> login(long protocolVersion, ByteBuffer payload, HandshakeResponseListener listener) {
if (!payload.hasRemaining()) {
return Optional.of(new AuthContext.Anonymous());
}
return Optional.empty();
}

@Override
public Optional<AuthContext> login(String payload, MetadataResponseListener listener) {
if (payload.length() == 0) {
return Optional.of(new AuthContext.Anonymous());
}
return Optional.empty();
}
}
36 changes: 36 additions & 0 deletions authentication/src/main/java/io/deephaven/auth/AuthContext.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
*/
package io.deephaven.auth;

import io.deephaven.base.log.LogOutput;
import io.deephaven.base.log.LogOutputAppendable;
import io.deephaven.io.log.impl.LogOutputStringImpl;

public abstract class AuthContext implements LogOutputAppendable {

@Override
public abstract LogOutput append(LogOutput logOutput);

@Override
final public String toString() {
return new LogOutputStringImpl().append(this).toString();
}

/**
* A trivial auth context that allows a user to do everything the APIs allow.
*/
public static class SuperUser extends AuthContext {
@Override
public LogOutput append(LogOutput logOutput) {
return logOutput.append("SuperUser");
}
}

public static class Anonymous extends AuthContext {
@Override
public LogOutput append(LogOutput logOutput) {
return logOutput.append("Anonymous");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.deephaven.auth;

/**
* An error occurred and this handshake to authenticate has failed for some reason. Details are not provided to the
* user.
*/
public class AuthenticationException extends Exception {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package io.deephaven.auth;

import java.nio.ByteBuffer;
import java.util.Optional;

/**
* Simple interface to handle incoming authentication requests from flight/barrage clients, via Handshake or the Flight
* Authentication header. This is intended to be a low-level way to handle incoming payload bytes.
*/
public interface AuthenticationRequestHandler {

/**
* This handler can be referred to via both Arrow Flight's original Auth and Auth2.
*
* To use via the original Arrow Flight Handshake, the request should be sent in a
* {@link io.deephaven.proto.backplane.grpc.WrappedAuthenticationRequest} with this handler's identity string.
*
* To use via Arrow Flight Auth 2's metadata header, then the
* {@link org.apache.arrow.flight.auth2.Auth2Constants#AUTHORIZATION_HEADER} should be prefixed with this handler's
* identity string.
*
* @return the type string used to identify the handler
*/
String getAuthType();

/**
* Given a protocol version (very likely to be zero) and payload bytes, if possible authenticate this user. If the
* handler can correctly decode the payload and confirm the user's identity, an appropriate UserContext should be
* returned. If the payload is correctly decoded and definitely isn't a valid user, an exception may be thrown. If
* there is ambiguity in decoding the payload (leading to apparent "not a valid user") or the payload cannot be
* decoded, an empty optional should be returned.
*
* Note that regular arrow flight clients cannot specify the protocolVersion; to be compatible with flight auth
* assume protocolVersion will be zero.
*
* @param protocolVersion Mostly unused, this is an allowed field to set on HandshakeRequests from the Flight gRPC
* call.
* @param payload The byte payload of the handshake, such as an encoded protobuf.
* @param listener The handshake response observer, which enables multi-request authentication.
* @return AuthContext for this user if applicable else Empty
*/
Optional<AuthContext> login(long protocolVersion, ByteBuffer payload, HandshakeResponseListener listener)
throws AuthenticationException;

/**
* Given a payload string, if possible authenticate this user. If the handler can correctly decode the payload and
* confirm the user's identity, an appropriate UserContext should be returned. If the payload is correctly decoded
* and definitely isn't a valid user, an exception may be thrown. If there is ambiguity in decoding the payload
* (leading to apparent "not a valid user") or the payload cannot be decoded, an empty optional should be returned.
*
* Note that metadata can only be sent with the initial gRPC response; multi-message authentication via gRPC
* metadata headers require multiple gRPC call attempts.
*
* @param payload The byte payload of the {@code Authorization} header, such as an encoded protobuf or b64 encoded
* string.
* @param listener The metadata response observer, which enables multi-request authentication.
* @return AuthContext for this user if applicable else Empty
*/
Optional<AuthContext> login(String payload, MetadataResponseListener listener)
throws AuthenticationException;

interface HandshakeResponseListener {
void respond(long protocolVersion, ByteBuffer payload);
}
interface MetadataResponseListener {
void addHeader(String key, String string);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package io.deephaven.auth;

import com.google.protobuf.CodedInputStream;
import com.google.protobuf.WireFormat;
import org.apache.arrow.flight.auth2.Auth2Constants;
import org.apache.arrow.flight.impl.Flight;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Optional;

import static com.google.protobuf.WireFormat.WIRETYPE_LENGTH_DELIMITED;

/**
* Manually decode the payload as a BasicAuth message, confirm that only tags 2 and 3 are present as strings, otherwise
* pass. This is stricter than a usual protobuf decode, under the assumption that FlightClient will always only write
* those two fields, and user code couldn't customize the payload further to repeatedly write those fields or any other
* field.
*
* Despite being stricter than a standard protobuf decode, this is also very generic and might accidentally match the
* wrong message type. For this reason, this handler should not run until other more selective handlers have finished.
*
* This class delegates to a typed auth handler once it is certain that the payload appears to be a BasicAuth value.
*/
public class BasicAuthMarshaller implements AuthenticationRequestHandler {
public interface Handler {
Optional<AuthContext> login(String username, String password) throws AuthenticationException;
}

private final Handler handler;

public BasicAuthMarshaller(Handler handler) {
this.handler = handler;
}

@Override
public String getAuthType() {
return Auth2Constants.BASIC_PREFIX.trim();
}

@Override
public Optional<AuthContext> login(long protocolVersion, ByteBuffer payload, HandshakeResponseListener listener)
throws AuthenticationException {
CodedInputStream inputStream = CodedInputStream.newInstance(payload);

String username = null, password = null;

try {
while (!inputStream.isAtEnd()) {
int tag = inputStream.readTag();
switch (WireFormat.getTagFieldNumber(tag)) {
case Flight.BasicAuth.USERNAME_FIELD_NUMBER: {
if (username == null && WireFormat.getTagWireType(tag) == WIRETYPE_LENGTH_DELIMITED) {
username = inputStream.readString();
} else {
return Optional.empty();
}
break;
}
case Flight.BasicAuth.PASSWORD_FIELD_NUMBER: {
if (password == null && WireFormat.getTagWireType(tag) == WIRETYPE_LENGTH_DELIMITED) {
password = inputStream.readString();
} else {
return Optional.empty();
}
break;
}
default:
// Found an unexpected field; this is not a BasicAuth request.
return Optional.empty();
}
}
} catch (IOException e) {
return Optional.empty();
}
if (username != null && password != null) {
// This is likely to be an un-wrapped BasicAuth instance, attempt to read it and login with it
return handler.login(username, password);
}

return Optional.empty();
}

@Override
public Optional<AuthContext> login(String payload, MetadataResponseListener listener)
throws AuthenticationException {
// The value has the format Base64(<username>:<password>)
final String authDecoded = new String(Base64.getDecoder().decode(payload), StandardCharsets.UTF_8);
final int colonPos = authDecoded.indexOf(':');
if (colonPos == -1) {
return Optional.empty();
}

final String username = authDecoded.substring(0, colonPos);
final String password = authDecoded.substring(colonPos + 1);
return handler.login(username, password);
}
}
1 change: 1 addition & 0 deletions engine/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
description 'Engine API: Engine API module, suitable as a compile-time dependency for most queries'

dependencies {
api project(':authentication')
api project(':qst')
api project(':engine-chunk')
api project(':engine-context')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
package io.deephaven.engine.exceptions;

import io.deephaven.util.auth.AuthContext;
import io.deephaven.auth.AuthContext;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

Expand All @@ -30,7 +30,7 @@ private static String makeDescription(@NotNull String tableDescription,
@NotNull AuthContext authContext, @Nullable String reason) {
final StringBuilder sb = new StringBuilder();

sb.append(authContext.getLogRepresentation()).append(" may not access: ");
sb.append(authContext).append(" may not access: ");
sb.append(tableDescription);

if (reason != null && !reason.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
package io.deephaven.engine.exceptions;

import io.deephaven.UncheckedDeephavenException;
import io.deephaven.util.auth.AuthContext;
import io.deephaven.auth.AuthContext;

/**
* An {@link UncheckedDeephavenException} that indicates an issue with permissions.
Expand All @@ -24,14 +24,14 @@ public UncheckedPermissionException(Throwable cause) {
}

public UncheckedPermissionException(AuthContext context, String reason) {
super(context.getLogRepresentation() + ": " + reason);
super(context.toString() + ": " + reason);
}

public UncheckedPermissionException(AuthContext context, String reason, Throwable cause) {
super(context.getLogRepresentation() + ": " + reason, cause);
super(context.toString() + ": " + reason, cause);
}

public UncheckedPermissionException(AuthContext context, Throwable cause) {
super(context.getLogRepresentation(), cause);
super(context.toString(), cause);
}
}
Loading

0 comments on commit 343e103

Please sign in to comment.