Merge branch 'stable-3.1' into stable-3.2
* stable-3.1: Bump Bazel version to 3.1.0 SignedToken: Use URL-safe encoding Align duct-tape and jna versions with testcontainers version Upgrade testcontainers to 1.14.1 Bump MySQL Connector/J version to 5.1.48 GerritSimulation: Support change creation/deletion Change-Id: Ib3e8b17acd1df1774896da6795a7790a52653102
This commit is contained in:
@@ -1 +1 @@
|
||||
3.0.0
|
||||
3.1.0
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"project": "${project}",
|
||||
"branch": "master",
|
||||
"subject": "Change"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"url": "http://HOSTNAME:HTTP_PORT/a/changes/",
|
||||
"project": "_PROJECT"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"create_empty_commit": "true"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{
|
||||
"url": "http://HOSTNAME:HTTP_PORT/a/changes/",
|
||||
"number": "_NUMBER"
|
||||
}
|
||||
]
|
||||
@@ -21,7 +21,7 @@ import io.gatling.core.structure.ScenarioBuilder
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class CloneUsingBothProtocols extends GitSimulation {
|
||||
private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(url).queue
|
||||
private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(keys).queue
|
||||
private val default: String = name
|
||||
|
||||
override def replaceOverride(in: String): String = {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (C) 2020 The Android Open Source Project
|
||||
//
|
||||
// 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 com.google.gerrit.scenarios
|
||||
|
||||
import io.gatling.core.Predef.{atOnceUsers, _}
|
||||
import io.gatling.core.feeder.FileBasedFeederBuilder
|
||||
import io.gatling.core.structure.ScenarioBuilder
|
||||
import io.gatling.http.Predef._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class CreateChange extends GerritSimulation {
|
||||
private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(keys).queue
|
||||
private val default: String = name
|
||||
private val numberKey = "_number"
|
||||
|
||||
val test: ScenarioBuilder = scenario(unique)
|
||||
.feed(data)
|
||||
.exec(httpRequest
|
||||
.body(ElFileBody(body)).asJson
|
||||
.check(regex("\"" + numberKey + "\":(\\d+),").saveAs(numberKey)))
|
||||
.exec(session => {
|
||||
deleteChange.number = Some(session(numberKey).as[Int])
|
||||
session
|
||||
})
|
||||
|
||||
private val createProject = new CreateProject(default)
|
||||
private val deleteProject = new DeleteProject(default)
|
||||
private val deleteChange = new DeleteChange
|
||||
|
||||
setUp(
|
||||
createProject.test.inject(
|
||||
atOnceUsers(1)
|
||||
),
|
||||
test.inject(
|
||||
nothingFor(2 seconds),
|
||||
atOnceUsers(1)
|
||||
),
|
||||
deleteChange.test.inject(
|
||||
nothingFor(6 seconds),
|
||||
atOnceUsers(1)
|
||||
),
|
||||
deleteProject.test.inject(
|
||||
nothingFor(8 seconds),
|
||||
atOnceUsers(1)
|
||||
),
|
||||
).protocols(httpProtocol)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import io.gatling.core.feeder.FileBasedFeederBuilder
|
||||
import io.gatling.core.structure.ScenarioBuilder
|
||||
|
||||
class CreateProject extends ProjectSimulation {
|
||||
private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(url).queue
|
||||
private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(keys).queue
|
||||
|
||||
def this(default: String) {
|
||||
this()
|
||||
@@ -28,7 +28,7 @@ class CreateProject extends ProjectSimulation {
|
||||
|
||||
val test: ScenarioBuilder = scenario(unique)
|
||||
.feed(data)
|
||||
.exec(httpRequest)
|
||||
.exec(httpRequest.body(RawFileBody(body)).asJson)
|
||||
|
||||
setUp(
|
||||
test.inject(
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2020 The Android Open Source Project
|
||||
//
|
||||
// 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 com.google.gerrit.scenarios
|
||||
|
||||
import io.gatling.core.Predef.{atOnceUsers, _}
|
||||
import io.gatling.core.feeder.FileBasedFeederBuilder
|
||||
import io.gatling.core.structure.ScenarioBuilder
|
||||
import io.gatling.http.Predef.http
|
||||
|
||||
class DeleteChange extends GerritSimulation {
|
||||
private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(keys).queue
|
||||
var number: Option[Int] = None
|
||||
|
||||
val test: ScenarioBuilder = scenario(unique)
|
||||
.feed(data)
|
||||
.exec(session => {
|
||||
if (number.nonEmpty) {
|
||||
session.set("number", number.get)
|
||||
} else {
|
||||
session
|
||||
}
|
||||
})
|
||||
.exec(http(unique).delete("${url}${number}"))
|
||||
|
||||
setUp(
|
||||
test.inject(
|
||||
atOnceUsers(1)
|
||||
),
|
||||
).protocols(httpProtocol)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import io.gatling.core.feeder.FileBasedFeederBuilder
|
||||
import io.gatling.core.structure.ScenarioBuilder
|
||||
|
||||
class DeleteProject extends ProjectSimulation {
|
||||
private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(url).queue
|
||||
private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(keys).queue
|
||||
|
||||
def this(default: String) {
|
||||
this()
|
||||
|
||||
@@ -26,7 +26,9 @@ class GerritSimulation extends Simulation {
|
||||
private val pack: String = this.getClass.getPackage.getName
|
||||
private val path: String = pack.replaceAllLiterally(".", "/")
|
||||
protected val name: String = this.getClass.getSimpleName
|
||||
protected val resource: String = s"data/$path/$name.json"
|
||||
private val pathName: String = s"data/$path/$name"
|
||||
protected val resource: String = s"$pathName.json"
|
||||
protected val body: String = s"$pathName-body.json"
|
||||
protected val unique: String = name + "-" + this.hashCode()
|
||||
|
||||
protected val httpRequest: HttpRequestBuilder = http(unique).post("${url}")
|
||||
@@ -34,12 +36,22 @@ class GerritSimulation extends Simulation {
|
||||
conf.httpConfiguration.userName,
|
||||
conf.httpConfiguration.password)
|
||||
|
||||
protected val url: PartialFunction[(String, Any), Any] = {
|
||||
protected val keys: PartialFunction[(String, Any), Any] = {
|
||||
case ("url", url) =>
|
||||
var in = replaceOverride(url.toString)
|
||||
in = replaceProperty("hostname", "localhost", in)
|
||||
in = replaceProperty("http_port", 8080, in)
|
||||
replaceProperty("ssh_port", 29418, in)
|
||||
case ("number", number) =>
|
||||
val precedes = replaceKeyWith("_number", 0, number.toString)
|
||||
replaceProperty("number", 1, precedes)
|
||||
case ("project", project) =>
|
||||
val precedes = replaceKeyWith("_project", name, project.toString)
|
||||
replaceProperty("project", precedes)
|
||||
}
|
||||
|
||||
private def replaceProperty(term: String, in: String): String = {
|
||||
replaceProperty(term, term, in)
|
||||
}
|
||||
|
||||
protected def replaceProperty(term: String, default: Any, in: String): String = {
|
||||
@@ -66,7 +78,7 @@ class GerritSimulation extends Simulation {
|
||||
* Meant to be optionally overridden by plugins or other extensions.
|
||||
* Such potential overriding methods, such as the example below,
|
||||
* typically return resulting call(s) to [[replaceProperty()]].
|
||||
* This is usually similar to how [[url]] is implemented above.
|
||||
* This is usually similar to how [[keys]] is implemented above.
|
||||
*
|
||||
* <pre>
|
||||
* override def replaceOverride(in: String): String = {
|
||||
|
||||
@@ -21,7 +21,7 @@ import io.gatling.core.structure.ScenarioBuilder
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class ReplayRecordsFromFeeder extends GitSimulation {
|
||||
private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(url).circular
|
||||
private val data: FileBasedFeederBuilder[Any]#F#F = jsonFile(resource).convert(keys).circular
|
||||
private val default: String = name
|
||||
|
||||
override def replaceOverride(in: String): String = {
|
||||
|
||||
28
java/com/google/gerrit/server/mail/CheckTokenException.java
Normal file
28
java/com/google/gerrit/server/mail/CheckTokenException.java
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright 2020 The Android Open Source Project
|
||||
//
|
||||
// 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 com.google.gerrit.server.mail;
|
||||
|
||||
/** Indicates the SignedToken is invalid */
|
||||
public class CheckTokenException extends Exception {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
CheckTokenException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
CheckTokenException(final String message, final Throwable why) {
|
||||
super(message, why);
|
||||
}
|
||||
}
|
||||
@@ -88,24 +88,29 @@ public class SignedToken {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a returned token.
|
||||
* Validate a returned token. If the token is valid then return a {@link ValidToken}, else will
|
||||
* throw {@link XsrfException} when it's an unexpected token overflow or {@link
|
||||
* CheckTokenException} when it's an illegal token string format.
|
||||
*
|
||||
* @param tokenString a token string previously created by this class.
|
||||
* @param text text that must have been used during {@link #newToken(String)} in order for the
|
||||
* token to be valid. If null the text will be taken from the token string itself.
|
||||
* @return true if the token is valid; false if the token is null, the empty string, has expired,
|
||||
* does not match the text supplied, or is a forged token.
|
||||
* @return the token which is valid.
|
||||
* @throws XsrfException the JVM doesn't support the necessary algorithms to generate a token.
|
||||
* XSRF services are simply not available.
|
||||
* @throws CheckTokenException throws when token is null, the empty string, has expired, does not
|
||||
* match the text supplied, or is a forged token.
|
||||
*/
|
||||
ValidToken checkToken(final String tokenString, final String text) throws XsrfException {
|
||||
public ValidToken checkToken(final String tokenString, final String text)
|
||||
throws XsrfException, CheckTokenException {
|
||||
|
||||
if (tokenString == null || tokenString.length() == 0) {
|
||||
return null;
|
||||
throw new CheckTokenException("Empty token");
|
||||
}
|
||||
|
||||
final int s = tokenString.indexOf('$');
|
||||
if (s <= 0) {
|
||||
return null;
|
||||
throw new CheckTokenException("Token does not contain character '$'");
|
||||
}
|
||||
|
||||
final String recvText = tokenString.substring(s + 1);
|
||||
@@ -113,24 +118,25 @@ public class SignedToken {
|
||||
try {
|
||||
in = decodeBase64(tokenString.substring(0, s));
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
throw new CheckTokenException("Base64 decoding failed", e);
|
||||
}
|
||||
|
||||
if (in.length != tokenLength) {
|
||||
return null;
|
||||
throw new CheckTokenException("Token length mismatch");
|
||||
}
|
||||
|
||||
final int q = decodeInt(in, 0);
|
||||
final int c = decodeInt(in, INT_SZ) ^ q;
|
||||
final int n = now();
|
||||
if (maxAge > 0 && Math.abs(c - n) > maxAge) {
|
||||
return null;
|
||||
throw new CheckTokenException("Token is expired");
|
||||
}
|
||||
|
||||
final byte[] gen = new byte[tokenLength];
|
||||
System.arraycopy(in, 0, gen, 0, 2 * INT_SZ);
|
||||
computeToken(gen, text != null ? text : recvText);
|
||||
if (!Arrays.equals(gen, in)) {
|
||||
return null;
|
||||
throw new CheckTokenException("Token text mismatch");
|
||||
}
|
||||
|
||||
return new ValidToken(maxAge > 0 && c + (maxAge >> 1) <= n, recvText);
|
||||
@@ -164,11 +170,11 @@ public class SignedToken {
|
||||
}
|
||||
|
||||
private static byte[] decodeBase64(final String s) {
|
||||
return BaseEncoding.base64().decode(s);
|
||||
return BaseEncoding.base64Url().decode(s);
|
||||
}
|
||||
|
||||
private static String encodeBase64(final byte[] buf) {
|
||||
return BaseEncoding.base64().encode(buf);
|
||||
return BaseEncoding.base64Url().encode(buf);
|
||||
}
|
||||
|
||||
private static void encodeInt(final byte[] buf, final int o, final int v) {
|
||||
|
||||
@@ -50,7 +50,7 @@ public class SignedTokenEmailTokenVerifier implements EmailTokenVerifier {
|
||||
try {
|
||||
String payload = String.format("%s:%s", accountId, emailAddress);
|
||||
byte[] utf8 = payload.getBytes(UTF_8);
|
||||
String base64 = BaseEncoding.base64().encode(utf8);
|
||||
String base64 = BaseEncoding.base64Url().encode(utf8);
|
||||
return emailRegistrationToken.newToken(base64);
|
||||
} catch (XsrfException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
@@ -63,14 +63,14 @@ public class SignedTokenEmailTokenVerifier implements EmailTokenVerifier {
|
||||
ValidToken token;
|
||||
try {
|
||||
token = emailRegistrationToken.checkToken(tokenString, null);
|
||||
} catch (XsrfException err) {
|
||||
} catch (XsrfException | CheckTokenException err) {
|
||||
throw new InvalidTokenException(err);
|
||||
}
|
||||
if (token == null || token.getData() == null || token.getData().isEmpty()) {
|
||||
throw new InvalidTokenException();
|
||||
}
|
||||
|
||||
String payload = new String(BaseEncoding.base64().decode(token.getData()), UTF_8);
|
||||
String payload = new String(BaseEncoding.base64Url().decode(token.getData()), UTF_8);
|
||||
Matcher matcher = Pattern.compile("^([0-9]+):(.+@.+)$").matcher(payload);
|
||||
if (!matcher.matches()) {
|
||||
throw new InvalidTokenException();
|
||||
|
||||
@@ -1163,7 +1163,7 @@ public class AccountIT extends AbstractDaemonTest {
|
||||
@Test
|
||||
@GerritConfig(
|
||||
name = "auth.registerEmailPrivateKey",
|
||||
value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
|
||||
value = "HsOc6l_2lhS9G7sE_RsnS7Z6GJjdRDX14co=")
|
||||
public void addEmailSendsConfirmationEmail() throws Exception {
|
||||
String email = "new.email@example.com";
|
||||
EmailInput input = newEmailInput(email, false);
|
||||
@@ -1177,7 +1177,7 @@ public class AccountIT extends AbstractDaemonTest {
|
||||
@Test
|
||||
@GerritConfig(
|
||||
name = "auth.registerEmailPrivateKey",
|
||||
value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
|
||||
value = "HsOc6l_2lhS9G7sE-RsnS7Z6GJjdRDX14co=")
|
||||
public void addEmailToBeConfirmedToOwnAccount() throws Exception {
|
||||
TestAccount user = accountCreator.create();
|
||||
requestScopeOperations.setApiUser(user.id());
|
||||
@@ -1206,7 +1206,7 @@ public class AccountIT extends AbstractDaemonTest {
|
||||
@Test
|
||||
@GerritConfig(
|
||||
name = "auth.registerEmailPrivateKey",
|
||||
value = "HsOc6l+2lhS9G7sE/RsnS7Z6GJjdRDX14co=")
|
||||
value = "HsOc6l_2lhS9G7sE-RsnS7Z6GJjdRDX14co=")
|
||||
public void addEmailToBeConfirmedToOtherAccount() throws Exception {
|
||||
TestAccount user = accountCreator.create();
|
||||
String email = "me@example.com";
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
// Copyright (C) 2020 The Android Open Source Project
|
||||
//
|
||||
// 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 com.google.gerrit.acceptance.server.mail;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
|
||||
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import com.google.gerrit.acceptance.AbstractDaemonTest;
|
||||
import com.google.gerrit.server.mail.EmailTokenVerifier;
|
||||
import com.google.gerrit.server.mail.EmailTokenVerifier.InvalidTokenException;
|
||||
import com.google.gerrit.server.mail.SignedToken;
|
||||
import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
|
||||
import com.google.gerrit.testing.ConfigSuite;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.eclipse.jgit.lib.Config;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SignedTokenEmailTokenVerifierIT extends AbstractDaemonTest {
|
||||
@ConfigSuite.Default
|
||||
public static Config defaultConfig() {
|
||||
Config cfg = new Config();
|
||||
cfg.setString("auth", null, "registerEmailPrivateKey", SignedToken.generateRandomKey());
|
||||
return cfg;
|
||||
}
|
||||
|
||||
private SignedTokenEmailTokenVerifier signedTokenEmailTokenVerifier;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
signedTokenEmailTokenVerifier =
|
||||
server
|
||||
.getTestInjector()
|
||||
.getBinding(SignedTokenEmailTokenVerifier.class)
|
||||
.getProvider()
|
||||
.get();
|
||||
}
|
||||
|
||||
/** Test encode */
|
||||
@Test
|
||||
public void encode() throws Exception {
|
||||
String tokenString = signedTokenEmailTokenVerifier.encode(user.id(), user.email());
|
||||
int index = tokenString.indexOf("$");
|
||||
String text = tokenString.substring(index + 1);
|
||||
String textDecoded = new String(BaseEncoding.base64Url().decode(text), StandardCharsets.UTF_8);
|
||||
int pos = textDecoded.indexOf(":");
|
||||
assertThat(textDecoded.substring(0, pos)).isEqualTo(user.id().toString());
|
||||
assertThat(textDecoded.substring(pos + 1)).isEqualTo(user.email());
|
||||
}
|
||||
|
||||
/** Test decode */
|
||||
@Test
|
||||
public void decode() throws Exception {
|
||||
String tokenString = signedTokenEmailTokenVerifier.encode(user.id(), user.email());
|
||||
String tokenKey = tokenString.substring(0, tokenString.indexOf("$"));
|
||||
String text = user.id() + ":" + user.email();
|
||||
String invalidTokenString =
|
||||
tokenKey + "$" + BaseEncoding.base64Url().encode(text.getBytes(StandardCharsets.UTF_8));
|
||||
EmailTokenVerifier.ParsedToken parsedToken =
|
||||
signedTokenEmailTokenVerifier.decode(invalidTokenString);
|
||||
assertThat(parsedToken.getAccountId()).isEqualTo(user.id());
|
||||
assertThat(parsedToken.getEmailAddress()).isEqualTo(user.email());
|
||||
}
|
||||
|
||||
/** Test token format is wrong(without '$' to split key and text) */
|
||||
@Test
|
||||
public void invalidFormat() throws Exception {
|
||||
InvalidTokenException thrown =
|
||||
assertThrows(
|
||||
InvalidTokenException.class,
|
||||
() -> signedTokenEmailTokenVerifier.decode("Invalid token"));
|
||||
assertThat(thrown)
|
||||
.hasCauseThat()
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Token does not contain character '$'");
|
||||
}
|
||||
|
||||
/** Test input token string is empty or null */
|
||||
@Test
|
||||
public void emptyInput() throws Exception {
|
||||
InvalidTokenException thrownWithNull =
|
||||
assertThrows(InvalidTokenException.class, () -> signedTokenEmailTokenVerifier.decode(null));
|
||||
InvalidTokenException thrownWithEmpty =
|
||||
assertThrows(InvalidTokenException.class, () -> signedTokenEmailTokenVerifier.decode(""));
|
||||
assertThat(thrownWithNull).hasCauseThat().hasMessageThat().isEqualTo("Empty token");
|
||||
assertThat(thrownWithEmpty).hasCauseThat().hasMessageThat().isEqualTo("Empty token");
|
||||
}
|
||||
|
||||
/** Test token format is right but key is an illegal BASE64 string */
|
||||
@Test
|
||||
public void illegalTokenKey() throws Exception {
|
||||
InvalidTokenException thrown =
|
||||
assertThrows(
|
||||
InvalidTokenException.class,
|
||||
() -> signedTokenEmailTokenVerifier.decode("Illegal token key$...."));
|
||||
assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("Base64 decoding failed");
|
||||
}
|
||||
|
||||
/** Test token text not match the required pattern */
|
||||
@Test
|
||||
public void tokenTextPatternMismatch() throws Exception {
|
||||
String tokenString = signedTokenEmailTokenVerifier.encode(user.id(), user.email());
|
||||
String tokenKey = tokenString.substring(0, tokenString.indexOf("$"));
|
||||
String pattern = user.id() + ":" + user.email();
|
||||
String invalidTokenTextPattern = tokenKey + "$" + pattern.replace(":", "");
|
||||
InvalidTokenException thrown =
|
||||
assertThrows(
|
||||
InvalidTokenException.class,
|
||||
() -> signedTokenEmailTokenVerifier.decode(invalidTokenTextPattern));
|
||||
assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("Token text mismatch");
|
||||
}
|
||||
}
|
||||
172
javatests/com/google/gerrit/server/mail/SignedTokenTest.java
Normal file
172
javatests/com/google/gerrit/server/mail/SignedTokenTest.java
Normal file
@@ -0,0 +1,172 @@
|
||||
// Copyright (C) 2020 The Android Open Source Project
|
||||
//
|
||||
// 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 com.google.gerrit.server.mail;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
|
||||
|
||||
import java.util.Random;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SignedTokenTest {
|
||||
|
||||
private static final String REGISTER_EMAIL_PRIVATE_KEY =
|
||||
"R2Vycml0JTIwcmVnaXN0ZXJFbWFpbFByaXZhdGVLZXk=";
|
||||
private static final String URL_SAFE_REGISTER_EMAIL_PRIVATE_KEY =
|
||||
REGISTER_EMAIL_PRIVATE_KEY.replaceFirst("R2", "_-");
|
||||
private static final String URL_UNSAFE_REGISTER_EMAIL_PRIVATE_KEY_WITH_PLUS =
|
||||
REGISTER_EMAIL_PRIVATE_KEY.replaceFirst("R", "+");
|
||||
private static final String URL_UNSAFE_REGISTER_EMAIL_PRIVATE_KEY_WITH_SLASH =
|
||||
REGISTER_EMAIL_PRIVATE_KEY.replaceFirst("R", "/");
|
||||
|
||||
private static final int maxAge = 5;
|
||||
private static final String TEXT = "This is a text";
|
||||
private static final String FORGED_TEXT = "This is a forged text";
|
||||
private static final String FORGED_TOKEN = String.format("Zm9yZ2VkJTIwa2V5$%s", TEXT);
|
||||
|
||||
private SignedToken signedToken;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
signedToken = new SignedToken(maxAge, REGISTER_EMAIL_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test new token: the key is a normal BASE64 string without index of '62'(+ or _) or '63'(/ or -)
|
||||
*/
|
||||
@Test
|
||||
public void newTokenKeyDoesNotContainUnsafeChar() throws Exception {
|
||||
new SignedToken(maxAge, REGISTER_EMAIL_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
/** Test new token: the key is an URL safe BASE64 string with indexes of '62'(_) and '63'(-) */
|
||||
@Test
|
||||
public void newTokenWithUrlSafeBase64() throws Exception {
|
||||
new SignedToken(maxAge, URL_SAFE_REGISTER_EMAIL_PRIVATE_KEY);
|
||||
}
|
||||
|
||||
/** Test new token: the key is an URL unsafe BASE64 string with index of '62'(+) */
|
||||
@Test
|
||||
public void newTokenWithUrlUnsafeBase64Plus() throws Exception {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new SignedToken(maxAge, URL_UNSAFE_REGISTER_EMAIL_PRIVATE_KEY_WITH_PLUS));
|
||||
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"com.google.common.io.BaseEncoding$DecodingException: Unrecognized character: +");
|
||||
}
|
||||
|
||||
/** Test new token: the key is an URL unsafe BASE64 string with '63'(/) */
|
||||
@Test
|
||||
public void newTokenWithUrlUnsafeBase64Slash() throws Exception {
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(
|
||||
IllegalArgumentException.class,
|
||||
() -> new SignedToken(maxAge, URL_UNSAFE_REGISTER_EMAIL_PRIVATE_KEY_WITH_SLASH));
|
||||
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo(
|
||||
"com.google.common.io.BaseEncoding$DecodingException: Unrecognized character: /");
|
||||
}
|
||||
|
||||
/** Test check token: BASE64 encoding and decoding in a safe URL way */
|
||||
@Test
|
||||
public void checkToken() throws Exception {
|
||||
String token = signedToken.newToken(TEXT);
|
||||
ValidToken validToken = signedToken.checkToken(token, TEXT);
|
||||
assertThat(validToken).isNotNull();
|
||||
assertThat(validToken.getData()).isEqualTo(TEXT);
|
||||
}
|
||||
|
||||
/** Test check token: input token string is null */
|
||||
@Test
|
||||
public void checkTokenInputTokenNull() throws Exception {
|
||||
CheckTokenException thrown =
|
||||
assertThrows(CheckTokenException.class, () -> signedToken.checkToken(null, TEXT));
|
||||
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Empty token");
|
||||
}
|
||||
|
||||
/** Test check token: input token string is empty */
|
||||
@Test
|
||||
public void checkTokenInputTokenEmpty() throws Exception {
|
||||
CheckTokenException thrown =
|
||||
assertThrows(CheckTokenException.class, () -> signedToken.checkToken("", TEXT));
|
||||
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Empty token");
|
||||
}
|
||||
|
||||
/** Test check token: token string is not illegal with no '$' character */
|
||||
@Test
|
||||
public void checkTokenInputTokenNoDollarSplitChar() throws Exception {
|
||||
String token = signedToken.newToken(TEXT).replace("$", "¥");
|
||||
CheckTokenException thrown =
|
||||
assertThrows(CheckTokenException.class, () -> signedToken.checkToken(token, TEXT));
|
||||
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Token does not contain character '$'");
|
||||
}
|
||||
|
||||
/** Test check token: token string length is match but is not a legal BASE64 string */
|
||||
@Test
|
||||
public void checkTokenInputTokenKeyBase64DecodeFail() throws Exception {
|
||||
String token = signedToken.newToken(TEXT);
|
||||
String key = randomString(token.indexOf("$") + 1);
|
||||
String illegalBase64Token = key + "$" + TEXT;
|
||||
CheckTokenException thrown =
|
||||
assertThrows(
|
||||
CheckTokenException.class, () -> signedToken.checkToken(illegalBase64Token, TEXT));
|
||||
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Base64 decoding failed");
|
||||
}
|
||||
|
||||
/** Test check token: token is illegal with a forged key */
|
||||
@Test
|
||||
public void checkTokenForgedKey() throws Exception {
|
||||
CheckTokenException thrown =
|
||||
assertThrows(CheckTokenException.class, () -> signedToken.checkToken(FORGED_TOKEN, TEXT));
|
||||
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Token length mismatch");
|
||||
}
|
||||
|
||||
/** Test check token: token is illegal with a forged text */
|
||||
@Test
|
||||
public void checkTokenForgedText() throws Exception {
|
||||
CheckTokenException thrown =
|
||||
assertThrows(
|
||||
CheckTokenException.class,
|
||||
() -> {
|
||||
String token = signedToken.newToken(TEXT);
|
||||
signedToken.checkToken(token, FORGED_TEXT);
|
||||
});
|
||||
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Token text mismatch");
|
||||
}
|
||||
|
||||
private static String randomString(int length) {
|
||||
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
Random random = new Random();
|
||||
StringBuffer sb = new StringBuffer();
|
||||
for (int i = 0; i < length; i++) {
|
||||
int number = random.nextInt(62);
|
||||
sb.append(str.charAt(number));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -120,24 +120,24 @@ def declare_nongoogle_deps():
|
||||
sha1 = "dc13ae4faca6df981fc7aeb5a522d9db446d5d50",
|
||||
)
|
||||
|
||||
TESTCONTAINERS_VERSION = "1.14.0"
|
||||
TESTCONTAINERS_VERSION = "1.14.1"
|
||||
|
||||
maven_jar(
|
||||
name = "testcontainers",
|
||||
artifact = "org.testcontainers:testcontainers:" + TESTCONTAINERS_VERSION,
|
||||
sha1 = "c0d6aea93f4f7ff4b0d559e31308340eaa398798",
|
||||
sha1 = "defd04ff6ffc93e1ff988024048e8ba5bd298df3",
|
||||
)
|
||||
|
||||
maven_jar(
|
||||
name = "testcontainers-elasticsearch",
|
||||
artifact = "org.testcontainers:elasticsearch:" + TESTCONTAINERS_VERSION,
|
||||
sha1 = "6df7bc7cb5e99c6d9528ea28dd16dbb042b1beec",
|
||||
sha1 = "d682965bbf1334ef40720b4ad2eda2c12bf0b440",
|
||||
)
|
||||
|
||||
maven_jar(
|
||||
name = "duct-tape",
|
||||
artifact = "org.rnorth.duct-tape:duct-tape:1.0.7",
|
||||
sha1 = "a26b5d90d88c91321dc7a3734ea72d2fc019ebb6",
|
||||
artifact = "org.rnorth.duct-tape:duct-tape:1.0.8",
|
||||
sha1 = "92edc22a9ab2f3e17c9bf700aaee377d50e8b530",
|
||||
)
|
||||
|
||||
maven_jar(
|
||||
@@ -148,6 +148,6 @@ def declare_nongoogle_deps():
|
||||
|
||||
maven_jar(
|
||||
name = "jna",
|
||||
artifact = "net.java.dev.jna:jna:5.2.0",
|
||||
sha1 = "ed8b772eb077a9cb50e44e90899c66a9a6c00e67",
|
||||
artifact = "net.java.dev.jna:jna:5.5.0",
|
||||
sha1 = "0e0845217c4907822403912ad6828d8e0b256208",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user