auths) {
if (!isValid(token)) {
throw new IllegalArgumentException(
"Invalid authorization token '" + token + "': tokens must be non-empty printable"
- + " ASCII without ',', '|', ';', ':', or whitespace (structural delimiters in the"
+ + " ASCII without ',', ';', ':', or whitespace (structural delimiters in the"
+ " auth transport)");
}
}
diff --git a/geomesa-trino/geomesa-trino-plugin/src/main/java/org/locationtech/geomesa/trino/security/ExtraCredentialAuthorizationResolver.java b/geomesa-trino/geomesa-trino-plugin/src/main/java/org/locationtech/geomesa/trino/security/ExtraCredentialAuthorizationResolver.java
index f419122c918..0876c516ead 100644
--- a/geomesa-trino/geomesa-trino-plugin/src/main/java/org/locationtech/geomesa/trino/security/ExtraCredentialAuthorizationResolver.java
+++ b/geomesa-trino/geomesa-trino-plugin/src/main/java/org/locationtech/geomesa/trino/security/ExtraCredentialAuthorizationResolver.java
@@ -12,6 +12,7 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
+import java.util.Base64;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
@@ -20,12 +21,14 @@
/**
* {@link AuthorizationResolver} that reads a session's authorization tokens
- * straight from a Trino extra credential carrying a comma-delimited
- * list (e.g. {@code auths=basic,privileged}). Intended for deployments fronted by a
- * trusted service mesh that authenticates the caller and injects the
- * authoritative token set: the mesh rewrites its identity header (e.g.
- * {@code x-auths}) into Trino's {@code X-Trino-Extra-Credential} client
- * header, which Trino exposes here via {@link ConnectorIdentity#getExtraCredentials()}.
+ * from a Trino extra credential carrying a base64url-encoded
+ * token list. The decoded payload is a comma or whitespace delimited list.
+ * Base64url keeps the wire value free of the extraCredentials pair delimiters
+ * by construction. Intended for deployments fronted by a trusted service mesh
+ * that authenticates the caller and injects the authoritative token set:
+ * the mesh base64url-encodes its identity header (e.g. {@code x-auths}) into Trino's
+ * {@code X-Trino-Extra-Credential} client header, which Trino exposes here
+ * via {@link ConnectorIdentity#getExtraCredentials()}.
*
* Because the tokens flow through unchanged there is no mapping file to
* maintain and nothing to drift from the platform's clearances. Selected with:
@@ -102,12 +105,16 @@ private boolean secretMatches(ConnectorIdentity identity) {
return MessageDigest.isEqual(expectedSecret, presented.getBytes(StandardCharsets.UTF_8));
}
- private static void addTokens(Set out, String csv) {
- if (csv == null) return;
- // Split on pipes, commas, or whitespace. The JDBC datastore joins tokens
- // with pipes (extraCredentials values forbid spaces); a mesh header may use
- // a comma or space list. Accept all three.
- for (String token : csv.split("[\\s,|]+")) {
+ private static void addTokens(Set out, String encoded) {
+ if (encoded == null) return;
+ String decoded;
+ try {
+ decoded = new String(Base64.getUrlDecoder().decode(encoded.trim()), StandardCharsets.UTF_8);
+ } catch (IllegalArgumentException e) {
+ LOG.warn("Authorization extra credential is not valid base64url; resolving to no auths");
+ return;
+ }
+ for (String token : decoded.split("[\\s,]+")) {
String t = token.trim();
if (t.isEmpty()) continue;
if (!AuthTokens.isValid(t)) {
diff --git a/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/ExtraCredentialAuthorizationResolverTest.java b/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/ExtraCredentialAuthorizationResolverTest.java
index e2817e07217..9a0c5a5d231 100644
--- a/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/ExtraCredentialAuthorizationResolverTest.java
+++ b/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/ExtraCredentialAuthorizationResolverTest.java
@@ -11,6 +11,9 @@
import io.trino.spi.security.ConnectorIdentity;
import org.junit.jupiter.api.Test;
+import java.util.Base64;
+
+import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@@ -25,26 +28,22 @@ private static ExtraCredentialAuthorizationResolver resolver(Map
return new ExtraCredentialAuthorizationResolver(config);
}
+ private static String enc(String tokenList) {
+ return Base64.getUrlEncoder().withoutPadding()
+ .encodeToString(tokenList.getBytes(StandardCharsets.UTF_8));
+ }
+
@Test
void readsTokensFromDefaultCredential() {
var r = resolver(Map.of());
- assertThat(r.authorizationsFor(identity(Map.of("auths", "basic,privileged"))))
+ assertThat(r.authorizationsFor(identity(Map.of("auths", enc("basic,privileged")))))
.containsExactlyInAnyOrder("basic", "privileged");
}
@Test
void honorsConfiguredCredentialName() {
var r = resolver(Map.of(ExtraCredentialAuthorizationResolver.CREDENTIAL_KEY, "x-auths"));
- assertThat(r.authorizationsFor(identity(Map.of("x-auths", "basic,privileged"))))
- .containsExactlyInAnyOrder("basic", "privileged");
- }
-
- @Test
- void readsPipeDelimitedTokens() {
- // The JDBC datastore joins tokens with pipes (spaces are rejected by the
- // Trino JDBC extraCredentials value validation).
- var r = resolver(Map.of());
- assertThat(r.authorizationsFor(identity(Map.of("auths", "basic|privileged"))))
+ assertThat(r.authorizationsFor(identity(Map.of("x-auths", enc("basic,privileged")))))
.containsExactlyInAnyOrder("basic", "privileged");
}
@@ -52,40 +51,40 @@ void readsPipeDelimitedTokens() {
void readsSpaceOrCommaDelimitedTokens() {
// Robust to a mesh-injected header carrying a space or comma list too.
var r = resolver(Map.of());
- assertThat(r.authorizationsFor(identity(Map.of("auths", "basic, privileged"))))
+ assertThat(r.authorizationsFor(identity(Map.of("auths", enc("basic, privileged")))))
.containsExactlyInAnyOrder("basic", "privileged");
}
@Test
void whitespaceAndBlankTokensAreTrimmedAndDropped() {
var r = resolver(Map.of());
- assertThat(r.authorizationsFor(identity(Map.of("auths", " basic , privileged ,,"))))
+ assertThat(r.authorizationsFor(identity(Map.of("auths", enc(" basic , privileged ,,")))))
.containsExactlyInAnyOrder("basic", "privileged");
}
@Test
void missingCredentialFailsClosedEmpty() {
var r = resolver(Map.of());
- assertThat(r.authorizationsFor(identity(Map.of("other", "basic,privileged")))).isEmpty();
+ assertThat(r.authorizationsFor(identity(Map.of("other", enc("basic,privileged"))))).isEmpty();
}
@Test
void secretGateHonorsAuthsWhenSecretMatches() {
var r = resolver(Map.of(ExtraCredentialAuthorizationResolver.SECRET_KEY, "s3cr3t"));
- assertThat(r.authorizationsFor(identity(Map.of("secret", "s3cr3t", "auths", "basic privileged"))))
+ assertThat(r.authorizationsFor(identity(Map.of("secret", "s3cr3t", "auths", enc("basic privileged")))))
.containsExactlyInAnyOrder("basic", "privileged");
}
@Test
void secretGateFailsClosedWhenSecretWrong() {
var r = resolver(Map.of(ExtraCredentialAuthorizationResolver.SECRET_KEY, "s3cr3t"));
- assertThat(r.authorizationsFor(identity(Map.of("secret", "nope", "auths", "basic privileged")))).isEmpty();
+ assertThat(r.authorizationsFor(identity(Map.of("secret", "nope", "auths", enc("basic privileged"))))).isEmpty();
}
@Test
void secretGateFailsClosedWhenSecretMissing() {
var r = resolver(Map.of(ExtraCredentialAuthorizationResolver.SECRET_KEY, "s3cr3t"));
- assertThat(r.authorizationsFor(identity(Map.of("auths", "basic privileged")))).isEmpty();
+ assertThat(r.authorizationsFor(identity(Map.of("auths", enc("basic privileged"))))).isEmpty();
}
@Test
@@ -94,13 +93,23 @@ void emptyCredentialFailsClosedEmpty() {
assertThat(r.authorizationsFor(identity(Map.of("auths", "")))).isEmpty();
}
+ @Test
+ void pinnedWireEncodingDecodesToTokens() {
+ var r = resolver(Map.of());
+ assertThat(r.authorizationsFor(identity(Map.of("auths", "YmFzaWMscHJpdmlsZWdlZA"))))
+ .containsExactlyInAnyOrder("basic", "privileged");
+ }
+
+ @Test
+ void nonBase64CredentialFailsClosedEmpty() {
+ var r = resolver(Map.of());
+ assertThat(r.authorizationsFor(identity(Map.of("auths", "basic,privileged")))).isEmpty();
+ }
+
@Test
void tokensCarryingPairDelimitersAreDroppedFailClosed() {
- // ';'/':' survive the pipe/comma/whitespace split but delimit the extraCredentials
- // wire pairs — no legitimate producer can grant such a token (AuthTokens rejects
- // them at the source), so the resolver drops rather than honors it.
var r = resolver(Map.of());
- assertThat(r.authorizationsFor(identity(Map.of("auths", "basic|FOO:BAR|privileged"))))
+ assertThat(r.authorizationsFor(identity(Map.of("auths", enc("basic,FOO:BAR,privileged")))))
.containsExactlyInAnyOrder("basic", "privileged");
}
}
diff --git a/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/FileAuthorizationResolverTest.java b/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/FileAuthorizationResolverTest.java
index 5a85baa71b9..2d3ced7b56f 100644
--- a/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/FileAuthorizationResolverTest.java
+++ b/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/FileAuthorizationResolverTest.java
@@ -70,11 +70,11 @@ void whitespaceAndBlankTokensAreTrimmedAndDropped(@TempDir Path dir) throws Exce
@Test
void invalidTokensAreDroppedFailClosed(@TempDir Path dir) throws Exception {
- // A token carrying a transport delimiter ('|' here, or ':'/';'/interior space)
+ // A token carrying a transport delimiter (':'/';'/interior space)
// can't round-trip through the row-filter chain — dropped with a warning
// (narrowing = fail-closed) rather than honored.
Path f = dir.resolve("m.properties");
- Files.write(f, java.util.List.of("user.alice=basic,FOO|BAR,privileged"));
+ Files.write(f, java.util.List.of("user.alice=basic,FOO;BAR,privileged"));
var resolver = new FileAuthorizationResolver(f);
assertThat(resolver.authorizationsFor(identity("alice", Set.of())))
.containsExactlyInAnyOrder("basic", "privileged");
diff --git a/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/VisibilityRowFilterTest.java b/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/VisibilityRowFilterTest.java
index d3d33a7e119..6af97733016 100644
--- a/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/VisibilityRowFilterTest.java
+++ b/geomesa-trino/geomesa-trino-plugin/src/test/java/org/locationtech/geomesa/trino/security/VisibilityRowFilterTest.java
@@ -50,7 +50,7 @@ void doubleQuoteInColumnIsDoubled() {
@Test
void authTokenContainingTransportDelimiterIsRejected() {
- for (String bad : List.of("FOO,BAR", "FOO|BAR", "FOO;BAR", "FOO:BAR", "FOO BAR", "")) {
+ for (String bad : List.of("FOO,BAR", "FOO;BAR", "FOO:BAR", "FOO BAR", "")) {
assertThatThrownBy(() -> VisibilityRowFilter.conjunct("__vis__", List.of("basic", bad)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("authorization token");