Skip to content

Commit

Permalink
[JENKINS-50216] - Introduce a custom serializer for org.joda.time.Dat…
Browse files Browse the repository at this point in the history
…eTime
  • Loading branch information
oleg-nenashev committed Apr 16, 2018
1 parent a878b91 commit c47f472
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 4 deletions.
Expand Up @@ -17,6 +17,7 @@

import java.io.IOException;
import java.security.GeneralSecurityException;
import javax.annotation.CheckForNull;

import org.joda.time.DateTime;

Expand Down Expand Up @@ -49,6 +50,14 @@ public RemotableGoogleCredentials(
GoogleRobotCredentials credentials,
GoogleOAuth2ScopeRequirement requirement,
GoogleRobotCredentialsModule module) throws GeneralSecurityException {
this(credentials, requirement, module, false);
}

/* package */ RemotableGoogleCredentials(
GoogleRobotCredentials credentials,
GoogleOAuth2ScopeRequirement requirement,
GoogleRobotCredentialsModule module,
boolean mock) throws GeneralSecurityException {
super(checkNotNull(credentials).getProjectId(), checkNotNull(module));

this.username = credentials.getUsername();
Expand All @@ -59,7 +68,7 @@ public RemotableGoogleCredentials(
try {
Long rawExpiration = credential.getExpiresInSeconds();

if (Ordering.natural().nullsFirst().compare(
if (!mock && Ordering.natural().nullsFirst().compare(
rawExpiration, MINIMUM_DURATION_SECONDS) < 0) {
if (!credential.refreshToken()) {
throw new GeneralSecurityException(
Expand All @@ -70,7 +79,7 @@ public RemotableGoogleCredentials(
throw new GeneralSecurityException(
Messages.RemotableGoogleCredentials_NoAccessToken(), e);
}
this.accessToken = checkNotNull(credential.getAccessToken());
this.accessToken = mock ? "MOCKED" : checkNotNull(credential.getAccessToken());
this.expiration = new DateTime().plusSeconds(
checkNotNull(credential.getExpiresInSeconds()).intValue());
}
Expand Down Expand Up @@ -101,6 +110,30 @@ public String getUsername() {
}

/**
* Gets expiration time of the credentials token.
* @return Expiration time. {@code null} if time is not set, Token will be considered as expired.
* @since TODO
*/
@CheckForNull
public DateTime getTokenExpirationTime() {
return expiration;
}

/**
* Check if token is expired.
* @return {@code true} if token is expired.
* If {@link #expiration} deserialization fails, the token is always expired
* @since TODO
*/
public boolean isTokenExpired() {
if (expiration != null) {
return expiration.getMillis() - new DateTime().getMillis() <= 0;
} else {
return true;
}
}

/**
* {@inheritDoc}
*/
@Override
Expand All @@ -112,8 +145,9 @@ public Credential getGoogleCredential(
//
// TODO(mattmoor): Consider throwing an exception if the access token
// has expired.
long lifetimeSeconds =
(expiration.getMillis() - new DateTime().getMillis()) / 1000;
long lifetimeSeconds = expiration != null
? (expiration.getMillis() - new DateTime().getMillis()) / 1000
: 0; // If we fail to deserialize time, the token is expired

return new GoogleCredential.Builder()
.setTransport(getModule().getHttpTransport())
Expand Down
@@ -0,0 +1,99 @@
package com.google.jenkins.plugins.util;


import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import jenkins.model.Jenkins;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.event.Level;

import java.util.TimeZone;
import java.util.logging.Logger;

/**
* @author Oleg Nenashev
* @since TODO
*/
public class JodaDateTimeConverter implements Converter {

private static final Logger LOGGER = Logger.getLogger(JodaDateTimeConverter.class.getName());

@Initializer(before = InitMilestone.PLUGINS_LISTED)
public static void initConverter() {
// Overrides the default converters
Jenkins.XSTREAM2.registerConverter(new JodaDateTimeConverter(), 10);
}

@Override
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
DateTime value = (DateTime)source;
writer.addAttribute("millis", Long.toString(value.getMillis()));
writer.addAttribute("timezone", value.getChronology().getZone().getID());
}

@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
String millis = reader.getAttribute("millis");
if (millis != null) { // new format
String timezone = reader.getAttribute("timezone");
Long val = Long.parseLong(millis);
DateTimeZone tz = timezone != null ? DateTimeZone.forID(timezone) : null;
return new DateTime(val, tz);
}

//TODO: this thing may not work for other ISOChronology implementations, but in such case we will get null
// Old format
// <iMillis>1523889095013</iMillis>
// <iChronology class="org.joda.time.chrono.ISOChronology" resolves-to="org.joda.time.chrono.ISOChronology$Stub" serialization="custom">
// <org.joda.time.chrono.ISOChronology_-Stub>
// <org.joda.time.tz.CachedDateTimeZone resolves-to="org.joda.time.DateTimeZone$Stub" serialization="custom">
// <org.joda.time.DateTimeZone_-Stub>
// <string>Europe/Zurich</string>
// </org.joda.time.DateTimeZone_-Stub>
// </org.joda.time.tz.CachedDateTimeZone>
// </org.joda.time.chrono.ISOChronology_-Stub>
// </iChronology>
String timezone = null;
while (reader.hasMoreChildren()) {
reader.moveDown();
String name = reader.getNodeName();
if (name.equals("iMillis")) {
millis = reader.getValue();
} else if (name.equals("iChronology")) {
reader.moveDown();
reader.moveDown();
reader.moveDown();
reader.moveDown();
if ("string".equals(reader.getNodeName())) {
timezone = reader.getValue();
}
reader.moveUp();
reader.moveUp();
reader.moveUp();
reader.moveUp();
}
reader.moveUp();
}
if (millis != null && timezone != null) {
Long val = Long.parseLong(millis);
DateTimeZone tz = DateTimeZone.forID(timezone);
return new DateTime(val, tz);
}

//TODO: throw something meaningful?
throw new IllegalStateException("Unsupported format for: " + DateTime.class + " in " + context.currentObject());
}

@Override
public boolean canConvert(Class type) {
return DateTime.class == type;
}


}
@@ -0,0 +1,115 @@
/*
* Copyright 2018 CloudBees, Inc.
*
* 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.jenkins.plugins.credentials.oauth;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collection;

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import hudson.XmlFile;
import jenkins.model.Jenkins;
import org.joda.time.DateTime;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.jvnet.hudson.test.For;
import org.jvnet.hudson.test.Issue;
import org.jvnet.hudson.test.JenkinsRule;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

/**
* Tests which involve real Jenkins instance.
* @author Oleg Nenashev
* @see RemotableGoogleCredentialsTest
*/
@For(RemotableGoogleCredentials.class)
public class RemotableGoogleCredentialsIntegrationTest {

@Rule
public JenkinsRule j = new JenkinsRule();

@Rule
public TemporaryFolder tmp = new TemporaryFolder();

@Test
@Issue("JENKINS-50216")
public void checkSerializationRoundtrip() throws Exception {
File file = new File(tmp.getRoot(), "remotableGoogleCredentials.xml");
XmlFile xml = new XmlFile(Jenkins.XSTREAM2, file);

GoogleCredential googleCredential = new GoogleCredential();
googleCredential.setExpiresInSeconds(120L);
GoogleRobotCredentials creds = new GoogleRobotCredentialsTest.FakeGoogleCredentials(
"myproject", googleCredential);

RemotableGoogleCredentials credentials = new RemotableGoogleCredentials(creds, new Requirement(), new GoogleRobotCredentialsModule(), true);
xml.write(credentials);

// now Reload it
RemotableGoogleCredentials read = (RemotableGoogleCredentials) xml.read();
assertFalse("Deserialized token should not be considered as expired", read.isTokenExpired());
}

@Test
@Issue("JENKINS-50216")
public void shouldDeserializeOldFormat() throws Exception {
File file = new File(tmp.getRoot(), "remotableGoogleCredentials.xml");
try (InputStream istream = RemotableGoogleCredentialsTest.class.getResourceAsStream("jodaDateTimeXML.xml");
OutputStream ostream = new FileOutputStream(file)) {
org.apache.commons.io.IOUtils.copy(istream, ostream);
}

XmlFile xml = new XmlFile(Jenkins.XSTREAM2, file);
RemotableGoogleCredentials read = (RemotableGoogleCredentials) xml.read();
DateTime expiration = read.getTokenExpirationTime();
assertNotNull("Expiration token should be read from the Old XML format", expiration);
assertEquals(1523889095013L, expiration.getMillis());
assertEquals("Europe/Zurich", expiration.getZone().getID());
}

@Test
@Issue("JENKINS-50216")
public void shouldDeserializeBrokenXMLAsNull() throws Exception {
File file = new File(tmp.getRoot(), "remotableGoogleCredentials.xml");
try (InputStream istream = RemotableGoogleCredentialsTest.class.getResourceAsStream("jodaDateTimeBroken.xml");
OutputStream ostream = new FileOutputStream(file)) {
org.apache.commons.io.IOUtils.copy(istream, ostream);
}

XmlFile xml = new XmlFile(Jenkins.XSTREAM2, file);
RemotableGoogleCredentials read = (RemotableGoogleCredentials) xml.read();
DateTime expiration = read.getTokenExpirationTime();
assertNull("Expiration token should be null", expiration);
assertTrue("Deserialized token should be considered as expired", read.isTokenExpired());
}

private static class Requirement extends GoogleOAuth2ScopeRequirement {

@Override
public Collection<String> getScopes() {
return Arrays.asList("foo", "bar");
}
}
}
@@ -0,0 +1,19 @@
<?xml version='1.0' encoding='UTF-8'?>
<com.google.jenkins.plugins.credentials.oauth.RemotableGoogleCredentials>
<module/>
<projectId>myproject</projectId>
<username>mattomata</username>
<accessToken>&lt;MOCKED&gt;</accessToken>
<expiration>
<iMillis>1523889095013</iMillis>
<iChronology2 class="org.joda.time.chrono.ISOChronology" resolves-to="org.joda.time.chrono.ISOChronology$Stub" serialization="custom">
<org.joda.time.chrono.ISOChronology_-Stub>
<org.joda.time.tz.CachedDateTimeZone resolves-to="org.joda.time.DateTimeZone$Stub" serialization="custom">
<org.joda.time.DateTimeZone_-Stub>
<string>Europe/Zurich</string>
</org.joda.time.DateTimeZone_-Stub>
</org.joda.time.tz.CachedDateTimeZone>
</org.joda.time.chrono.ISOChronology_-Stub>
</iChronology2>
</expiration>
</com.google.jenkins.plugins.credentials.oauth.RemotableGoogleCredentials>
@@ -0,0 +1,19 @@
<?xml version='1.0' encoding='UTF-8'?>
<com.google.jenkins.plugins.credentials.oauth.RemotableGoogleCredentials>
<module/>
<projectId>myproject</projectId>
<username>mattomata</username>
<accessToken>&lt;MOCKED&gt;</accessToken>
<expiration>
<iMillis>1523889095013</iMillis>
<iChronology class="org.joda.time.chrono.ISOChronology" resolves-to="org.joda.time.chrono.ISOChronology$Stub" serialization="custom">
<org.joda.time.chrono.ISOChronology_-Stub>
<org.joda.time.tz.CachedDateTimeZone resolves-to="org.joda.time.DateTimeZone$Stub" serialization="custom">
<org.joda.time.DateTimeZone_-Stub>
<string>Europe/Zurich</string>
</org.joda.time.DateTimeZone_-Stub>
</org.joda.time.tz.CachedDateTimeZone>
</org.joda.time.chrono.ISOChronology_-Stub>
</iChronology>
</expiration>
</com.google.jenkins.plugins.credentials.oauth.RemotableGoogleCredentials>

0 comments on commit c47f472

Please sign in to comment.