This is an automated email from the ASF dual-hosted git repository. ctubbsii pushed a commit to branch 2.1 in repository https://gitbox.apache.org/repos/asf/accumulo.git
The following commit(s) were added to refs/heads/2.1 by this push: new f54a2b214a Fix CredentialProviderToken serialization (#4580) f54a2b214a is described below commit f54a2b214ab5ac38452536d5c9fd992813b1ba45 Author: Christopher Tubbs <ctubb...@apache.org> AuthorDate: Wed Jul 24 09:07:08 2024 -0400 Fix CredentialProviderToken serialization (#4580) * Fix the serialization of CredentialProviderToken, so it serializes the name and credential provider paths, rather than the resolved password from the provider * Add comments to reserve specific magic numbers/versions for serialization to separate it from PasswordToken's serialization * Mark internal method as private * Also fix issue in CreateToken where only the most recent property was being used to initialize a token, instead of all the properties * Add test of end-to-end serialization and deserialization * Use var in a few more places * Update test keystore with unique passwords for each jceks entry * Document hadoop commands for modifying the jceks files This fixes #4573 --- .../security/tokens/CredentialProviderToken.java | 26 ++++++++- .../core/client/security/tokens/PasswordToken.java | 1 + .../org/apache/accumulo/core/util/CreateToken.java | 2 +- .../tokens/CredentialProviderTokenTest.java | 65 ++++++++++++++++----- core/src/test/resources/passwords.jceks | Bin 963 -> 967 bytes 5 files changed, 78 insertions(+), 16 deletions(-) diff --git a/core/src/main/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderToken.java b/core/src/main/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderToken.java index 4db45c8f4e..66af59395b 100644 --- a/core/src/main/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderToken.java +++ b/core/src/main/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderToken.java @@ -20,6 +20,8 @@ package org.apache.accumulo.core.client.security.tokens; import static java.util.Objects.requireNonNull; +import java.io.DataInput; +import java.io.DataOutput; import java.io.IOException; import java.nio.CharBuffer; import java.util.LinkedHashSet; @@ -46,7 +48,7 @@ public class CredentialProviderToken extends PasswordToken { setWithCredentialProviders(name, credentialProviders); } - protected void setWithCredentialProviders(String name, String credentialProviders) + private void setWithCredentialProviders(String name, String credentialProviders) throws IOException { this.name = name; this.credentialProviders = credentialProviders; @@ -63,6 +65,25 @@ public class CredentialProviderToken extends PasswordToken { setPassword(CharBuffer.wrap(password)); } + @Override + public void readFields(DataInput arg0) throws IOException { + // -1000 to -1999 is reserved for CredentialProviderToken; see PasswordToken + int version = arg0.readInt(); + if (version != -1000) { + throw new IOException("Invalid CredentialProviderToken"); + } + name = arg0.readUTF(); + credentialProviders = arg0.readUTF(); + setWithCredentialProviders(name, credentialProviders); + } + + @Override + public void write(DataOutput arg0) throws IOException { + arg0.writeInt(-1000); + arg0.writeUTF(name); + arg0.writeUTF(credentialProviders); + } + /** * @return Name used to extract Accumulo user password from CredentialProvider * @@ -114,7 +135,8 @@ public class CredentialProviderToken extends PasswordToken { @Override public CredentialProviderToken clone() { CredentialProviderToken clone = (CredentialProviderToken) super.clone(); - clone.setPassword(this.getPassword()); + clone.name = name; + clone.credentialProviders = credentialProviders; return clone; } diff --git a/core/src/main/java/org/apache/accumulo/core/client/security/tokens/PasswordToken.java b/core/src/main/java/org/apache/accumulo/core/client/security/tokens/PasswordToken.java index 8390f0f6e1..45f51cc4de 100644 --- a/core/src/main/java/org/apache/accumulo/core/client/security/tokens/PasswordToken.java +++ b/core/src/main/java/org/apache/accumulo/core/client/security/tokens/PasswordToken.java @@ -89,6 +89,7 @@ public class PasswordToken implements AuthenticationToken { int version = arg0.readInt(); // -1 is null, consistent with legacy format; legacy format length must be >= -1 // so, use -2 as a magic number to indicate the new format + // -1000 to -1999 is reserved for CredentialProviderToken if (version == -1) { password = null; } else if (version == -2) { diff --git a/core/src/main/java/org/apache/accumulo/core/util/CreateToken.java b/core/src/main/java/org/apache/accumulo/core/util/CreateToken.java index a545c0d92d..f17cf675d6 100644 --- a/core/src/main/java/org/apache/accumulo/core/util/CreateToken.java +++ b/core/src/main/java/org/apache/accumulo/core/util/CreateToken.java @@ -107,8 +107,8 @@ public class CreateToken implements KeywordExecutable { } } props.put(tp.getKey(), input); - token.init(props); } + token.init(props); System.out.println("auth.type = " + opts.tokenClassName); System.out.println("auth.principal = " + principal); System.out.println("auth.token = " + ClientProperty.encodeToken(token)); diff --git a/core/src/test/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderTokenTest.java b/core/src/test/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderTokenTest.java index 722e0f7e64..17c1b19569 100644 --- a/core/src/test/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderTokenTest.java +++ b/core/src/test/java/org/apache/accumulo/core/client/security/tokens/CredentialProviderTokenTest.java @@ -22,8 +22,13 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.File; import java.net.URL; @@ -35,7 +40,11 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; public class CredentialProviderTokenTest { - // Keystore contains: {'root.password':'password', 'bob.password':'bob'} + // Keystore contains (no password): {'root.password':'secret', 'bob.password':'hush'} + // Some useful commands to update the file are: + // bin/hadoop credential list -provider localjceks://file/path/to/passwords.jceks + // bin/hadoop credential delete bob.password -provider localjceks://file/path/to/passwords.jceks + // bin/hadoop credential create bob.password -provider localjceks://file/path/to/passwords.jceks private static String keystorePath; @SuppressFBWarnings(value = "PATH_TRAVERSAL_IN", @@ -49,20 +58,50 @@ public class CredentialProviderTokenTest { @Test public void testPasswordsFromCredentialProvider() throws Exception { - CredentialProviderToken token = new CredentialProviderToken("root.password", keystorePath); + var token = new CredentialProviderToken("root.password", keystorePath); assertEquals("root.password", token.getName()); assertEquals(keystorePath, token.getCredentialProviders()); - assertArrayEquals("password".getBytes(UTF_8), token.getPassword()); + assertArrayEquals("secret".getBytes(UTF_8), token.getPassword()); token = new CredentialProviderToken("bob.password", keystorePath); - assertArrayEquals("bob".getBytes(UTF_8), token.getPassword()); + assertArrayEquals("hush".getBytes(UTF_8), token.getPassword()); + } + + @Test + public void testSerialization() throws Exception { + var token = new CredentialProviderToken("bob.password", keystorePath); + assertEquals("bob.password", token.getName()); + assertEquals(keystorePath, token.getCredentialProviders()); + assertArrayEquals("hush".getBytes(UTF_8), token.getPassword()); + byte[] serialized; + try (var baos = new ByteArrayOutputStream(); var out = new DataOutputStream(baos)) { + token.write(out); + serialized = baos.toByteArray(); + } + // verify the serialized form only contains exactly what is expected and nothing else + try (var bais = new ByteArrayInputStream(serialized); var in = new DataInputStream(bais)) { + assertEquals(-1000, in.readInt()); + assertEquals("bob.password", in.readUTF()); + assertEquals(keystorePath, in.readUTF()); + assertEquals(0, in.available()); + } + // verify deserialization to a new token + var token2 = new CredentialProviderToken(); + try (var bais = new ByteArrayInputStream(serialized); var in = new DataInputStream(bais)) { + token2.readFields(in); + } + assertEquals("bob.password", token2.getName()); + assertEquals(keystorePath, token2.getCredentialProviders()); + assertArrayEquals("hush".getBytes(UTF_8), token2.getPassword()); + assertNotSame(token, token2); + assertEquals(token, token2); } @Test public void testEqualityAfterInit() throws Exception { - CredentialProviderToken token = new CredentialProviderToken("root.password", keystorePath); + var token = new CredentialProviderToken("root.password", keystorePath); - CredentialProviderToken uninitializedToken = new CredentialProviderToken(); + var uninitializedToken = new CredentialProviderToken(); Properties props = new Properties(); props.put(CredentialProviderToken.NAME_PROPERTY, "root.password"); props.put(CredentialProviderToken.CREDENTIAL_PROVIDERS_PROPERTY, keystorePath); @@ -73,8 +112,8 @@ public class CredentialProviderTokenTest { @Test public void cloneReturnsCorrectObject() throws Exception { - CredentialProviderToken token = new CredentialProviderToken("root.password", keystorePath); - CredentialProviderToken clone = token.clone(); + var token = new CredentialProviderToken("root.password", keystorePath); + var clone = token.clone(); assertEquals(token, clone); assertArrayEquals(token.getPassword(), clone.getPassword()); @@ -82,22 +121,22 @@ public class CredentialProviderTokenTest { @Test public void missingProperties() { - CredentialProviderToken token = new CredentialProviderToken(); + var token = new CredentialProviderToken(); assertThrows(IllegalArgumentException.class, () -> token.init(new Properties())); } @Test public void missingNameProperty() { - CredentialProviderToken token = new CredentialProviderToken(); - Properties props = new Properties(); + var token = new CredentialProviderToken(); + var props = new Properties(); props.put(CredentialProviderToken.NAME_PROPERTY, "root.password"); assertThrows(IllegalArgumentException.class, () -> token.init(props)); } @Test public void missingProviderProperty() { - CredentialProviderToken token = new CredentialProviderToken(); - Properties props = new Properties(); + var token = new CredentialProviderToken(); + var props = new Properties(); props.put(CredentialProviderToken.CREDENTIAL_PROVIDERS_PROPERTY, keystorePath); assertThrows(IllegalArgumentException.class, () -> token.init(props)); } diff --git a/core/src/test/resources/passwords.jceks b/core/src/test/resources/passwords.jceks index 9bbb6bc203..d872ee59a7 100755 Binary files a/core/src/test/resources/passwords.jceks and b/core/src/test/resources/passwords.jceks differ