Implements access token cache with file persistence

This commit is contained in:
fmartelli 2015-07-31 12:20:53 +02:00
parent 2c6bb7a711
commit 8ad20f7780
4 changed files with 257 additions and 118 deletions

View File

@ -17,9 +17,6 @@ package com.microsoftopentechnologies.azure;
import static com.microsoft.windowsazure.management.configuration.ManagementConfiguration.SUBSCRIPTION_CLOUD_CREDENTIALS;
import com.microsoft.aad.adal4j.AuthenticationContext;
import com.microsoft.aad.adal4j.AuthenticationResult;
import com.microsoft.aad.adal4j.ClientCredential;
import com.microsoft.azure.management.compute.ComputeManagementClient;
import com.microsoft.azure.management.compute.ComputeManagementService;
import com.microsoft.azure.management.network.NetworkResourceProviderClient;
@ -28,30 +25,18 @@ import com.microsoft.azure.management.resources.ResourceManagementClient;
import com.microsoft.azure.management.resources.ResourceManagementService;
import com.microsoft.azure.management.storage.StorageManagementClient;
import com.microsoft.azure.management.storage.StorageManagementService;
import java.net.URI;
import java.net.URISyntaxException;
import com.microsoft.windowsazure.Configuration;
import com.microsoft.windowsazure.credentials.TokenCloudCredentials;
import com.microsoft.windowsazure.management.ManagementClient;
import com.microsoft.windowsazure.management.ManagementService;
import com.microsoft.windowsazure.management.configuration.ManagementConfiguration;
import com.microsoftopentechnologies.azure.exceptions.AzureCloudException;
import com.microsoftopentechnologies.azure.exceptions.UnrecoverableCloudException;
import com.microsoftopentechnologies.azure.util.AccessToken;
import com.microsoftopentechnologies.azure.util.Constants;
import com.microsoftopentechnologies.azure.util.TokenCache;
import hudson.slaves.Cloud;
import java.net.MalformedURLException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.ServiceUnavailableException;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
/**
* Helper class to form the required client classes to call azure rest APIs
@ -63,8 +48,6 @@ public class ServiceDelegateHelper {
private static final Logger LOGGER = Logger.getLogger(ServiceDelegateHelper.class.getName());
private static final TokenCache tokenCache = new TokenCache();
public static Configuration getConfiguration(final AzureCloud cloud) throws AzureCloudException {
try {
return loadConfiguration(
@ -122,39 +105,6 @@ public class ServiceDelegateHelper {
azureCloud.getServiceManagementURL());
}
private static AuthenticationResult getAccessTokenFromServicePrincipalCredentials(
final String clientId,
final String clientSecret,
final String oauth2TokenEndpoint,
final String serviceManagementURL)
throws MalformedURLException, ExecutionException, InterruptedException, ServiceUnavailableException {
final ExecutorService service = Executors.newFixedThreadPool(1);
AuthenticationResult result = null;
try {
LOGGER.log(Level.INFO, "Aquiring access token: \n\t{0}\n\t{1}\n\t{2}",
new Object[] { oauth2TokenEndpoint, serviceManagementURL, clientId });
final ClientCredential credential = new ClientCredential(clientId, clientSecret);
final Future<AuthenticationResult> future = new AuthenticationContext(oauth2TokenEndpoint, false, service).
acquireToken(serviceManagementURL, credential, null);
result = future.get();
LOGGER.log(Level.INFO, "Aquired access token {0}", result.getAccessToken());
} finally {
service.shutdown();
}
if (result == null) {
throw new ServiceUnavailableException("authentication result was null");
}
return result;
}
/**
* Loads configuration object..
*
@ -181,53 +131,14 @@ public class ServiceDelegateHelper {
Thread.currentThread().setContextClassLoader(AzureManagementServiceDelegate.class.getClassLoader());
try {
final String url;
if (StringUtils.isBlank(serviceManagementURL)) {
url = Constants.DEFAULT_MANAGEMENT_URL;
} else {
url = serviceManagementURL;
}
final Configuration config = TokenCache.getInstance(
subscriptionId, clientId, clientSecret, oauth2TokenEndpoint, serviceManagementURL).get().
getConfiguration();
URI managementURI = new URI(url);
LOGGER.log(Level.INFO, "Configuration token: {0}", TokenCloudCredentials.class.cast(
config.getProperty(SUBSCRIPTION_CLOUD_CREDENTIALS)).getToken());
synchronized (tokenCache) {
AccessToken accessToken = tokenCache.get();
if (accessToken == null) {
// reset configuration instance: renew token
Configuration.setInstance(null);
final AuthenticationResult authres = getAccessTokenFromServicePrincipalCredentials(
clientId,
clientSecret,
oauth2TokenEndpoint,
url);
LOGGER.log(Level.INFO,
"Authentication result:\n\taccess token: {0}\n\trefresh token: {1}\n\tExpires On: {2}",
new Object[] {
authres.getAccessToken(), authres.getRefreshToken(), authres.getExpiresOnDate() });
accessToken = tokenCache.set(authres);
}
final Configuration config = ManagementConfiguration.configure(
null,
managementURI,
subscriptionId,
accessToken.toString());
LOGGER.log(Level.INFO, "Configuration token: {0}", TokenCloudCredentials.class.cast(
config.getProperty(SUBSCRIPTION_CLOUD_CREDENTIALS)).getToken());
return config;
}
} catch (URISyntaxException e) {
throw new AzureCloudException(
"The syntax of the Url in the publish settings file is incorrect.", e);
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Loading connection configuration parameters", e);
throw new AzureCloudException("Cannot obtain OAuth 2.0 access token", e);
return config;
} finally {
Thread.currentThread().setContextClassLoader(thread);
}

View File

@ -239,7 +239,7 @@ public class AzureSSHLauncher extends ComputerLauncher {
private int executeRemoteCommand(final Session jschSession, final String command, final PrintStream logger) {
ChannelExec channel = null;
LOGGER.info("AzureSSHLauncher: executeRemoteCommand: start");
LOGGER.info("AzureSSHLauncher: executeRemoteCommand: starts");
try {
channel = createExecChannel(jschSession, command);
final InputStream inputStream = channel.getInputStream();
@ -249,33 +249,37 @@ public class AzureSSHLauncher extends ComputerLauncher {
try {
IOUtils.copy(inputStream, logger);
} finally {
inputStream.close();
IOUtils.closeQuietly(inputStream);
}
// Read from error stream
try {
IOUtils.copy(errorStream, logger);
} finally {
errorStream.close();
IOUtils.closeQuietly(errorStream);
}
if (!channel.isClosed()) {
try {
LOGGER.warning("AzureSSHLauncher: executeRemoteCommand:"
+ " Channel is not yet closed , waiting for 10 seconds");
LOGGER.log(Level.WARNING,
"{0}: executeRemoteCommand: Channel is not yet closed, waiting for 10 seconds",
this.getClass().getSimpleName());
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
//ignore error
}
}
LOGGER.info("AzureSSHLauncher: executeRemoteCommand: ends successfully");
return channel.getExitStatus();
} catch (JSchException jse) {
LOGGER.log(Level.SEVERE,
"AzureSSHLauncher: executeRemoteCommand: got exception while executing remote command\n" + command,
jse);
} catch (IOException ex) {
LOGGER.log(Level.WARNING, "IO failure during running {0}", command);
LOGGER.log(Level.WARNING, "IO failure running {0}", command);
} catch (Exception e) {
LOGGER.log(Level.WARNING, "Unexpected exception running {0}", command);
} finally {
if (channel != null) {
channel.disconnect();

View File

@ -16,33 +16,63 @@
package com.microsoftopentechnologies.azure.util;
import com.microsoft.aad.adal4j.AuthenticationResult;
import com.microsoft.windowsazure.Configuration;
import com.microsoft.windowsazure.management.configuration.ManagementConfiguration;
import com.microsoftopentechnologies.azure.exceptions.AzureCloudException;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Calendar;
import java.util.Date;
public class AccessToken {
public class AccessToken implements Serializable {
final String value;
private static final long serialVersionUID = 1L;
final Date expiration;
private final String subscriptionId;
public static AccessToken load(final AuthenticationResult authres) {
return new AccessToken(authres.getAccessToken(), authres.getExpiresOnDate());
private final String serviceManagementUrl;
private final String token;
private final Date expiration;
AccessToken(
final String subscriptionId, final String serviceManagementUrl, final AuthenticationResult authres) {
this.subscriptionId = subscriptionId;
this.serviceManagementUrl = serviceManagementUrl;
this.token = authres.getAccessToken();
this.expiration = authres.getExpiresOnDate();
}
private AccessToken(final String value, final Date expiration) {
this.value = value;
this.expiration = expiration;
public Configuration getConfiguration() throws AzureCloudException {
try {
return ManagementConfiguration.configure(
null,
new URI(serviceManagementUrl),
subscriptionId,
token);
} catch (URISyntaxException e) {
throw new AzureCloudException("The syntax of the Url in the publish settings file is incorrect.", e);
} catch (IOException e) {
throw new AzureCloudException("Error updating authentication configuration", e);
}
}
public Date getExpirationDate() {
return expiration;
}
public boolean isExpiring() {
final Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
cal.add(Calendar.MINUTE, 10);
cal.add(Calendar.MINUTE, 5);
return expiration.before(cal.getTime());
}
@Override
public String toString() {
return value;
return token;
}
}

View File

@ -15,22 +15,216 @@
*/
package com.microsoftopentechnologies.azure.util;
import com.microsoft.aad.adal4j.AuthenticationContext;
import com.microsoft.aad.adal4j.AuthenticationResult;
import com.microsoft.aad.adal4j.ClientCredential;
import com.microsoft.windowsazure.Configuration;
import com.microsoftopentechnologies.azure.exceptions.AzureCloudException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.MalformedURLException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.model.Jenkins;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
public class TokenCache {
private AccessToken token = null;
private static final Logger LOGGER = Logger.getLogger(TokenCache.class.getName());
public AccessToken get() {
if (token != null && token.isExpiring()) {
token = null;
private static final Object tsafe = new Object();
private static TokenCache cache = null;
protected final String subscriptionId;
protected final String clientId;
protected final String clientSecret;
protected final String oauth2TokenEndpoint;
protected final String serviceManagementURL;
private final String path;
public static TokenCache getInstance(
final String subscriptionId,
final String clientId,
final String clientSecret,
final String oauth2TokenEndpoint,
final String serviceManagementURL) {
synchronized (tsafe) {
if (cache == null) {
cache = new TokenCache(
subscriptionId, clientId, clientSecret, oauth2TokenEndpoint, serviceManagementURL);
} else if (cache.subscriptionId == null || !cache.subscriptionId.equals(subscriptionId)
|| cache.clientId == null || !cache.clientId.equals(clientId)
|| cache.clientSecret == null || !cache.clientSecret.equals(clientSecret)
|| cache.oauth2TokenEndpoint == null || !cache.oauth2TokenEndpoint.equals(oauth2TokenEndpoint)
|| cache.serviceManagementURL == null || !cache.serviceManagementURL.equals(serviceManagementURL)) {
cache.clear();
cache = new TokenCache(
subscriptionId, clientId, clientSecret, oauth2TokenEndpoint, serviceManagementURL);
}
}
return token;
return cache;
}
public AccessToken set(final AuthenticationResult authres) {
token = AccessToken.load(authres);
private TokenCache(
final String subscriptionId,
final String clientId,
final String clientSecret,
final String oauth2TokenEndpoint,
final String serviceManagementURL) {
LOGGER.info("Instantiate new cache manager");
this.subscriptionId = subscriptionId;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.oauth2TokenEndpoint = oauth2TokenEndpoint;
if (StringUtils.isBlank(serviceManagementURL)) {
this.serviceManagementURL = Constants.DEFAULT_MANAGEMENT_URL;
} else {
this.serviceManagementURL = serviceManagementURL;
}
final String home = Jenkins.getInstance().root.getPath();
LOGGER.log(Level.INFO, "Cache home \"{0}\"", home);
final StringBuilder builder = new StringBuilder(home);
builder.append(File.separatorChar).append("azuretoken.txt");
this.path = builder.toString();
LOGGER.log(Level.INFO, "Cache file path \"{0}\"", path);
}
public AccessToken get() throws AzureCloudException {
LOGGER.log(Level.INFO, "Get token from cache");
synchronized (tsafe) {
AccessToken token = readTokenFile();
if (token == null || token.isExpiring()) {
LOGGER.log(Level.INFO, "Token is no longer valid ({0})",
token == null ? null : token.getExpirationDate());
clear();
token = getNewToken();
}
return token;
}
}
public final void clear() {
LOGGER.log(Level.INFO, "Remove cache file {0}", path);
FileUtils.deleteQuietly(new File(path));
}
private AccessToken readTokenFile() {
LOGGER.log(Level.INFO, "Read token from file {0}", path);
FileInputStream is = null;
ObjectInputStream objectIS = null;
try {
final File fileCache = new File(path);
if (fileCache.exists()) {
is = new FileInputStream(fileCache);
objectIS = new ObjectInputStream(is);
return AccessToken.class.cast(objectIS.readObject());
} else {
LOGGER.log(Level.INFO, "File {0} does not exist", path);
}
} catch (FileNotFoundException e) {
LOGGER.log(Level.SEVERE, "Cache file not found", e);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Error reading serialized object", e);
} catch (ClassNotFoundException e) {
LOGGER.log(Level.SEVERE, "Error deserializing object", e);
} finally {
IOUtils.closeQuietly(is);
IOUtils.closeQuietly(objectIS);
}
return null;
}
private boolean writeTokenFile(final AccessToken token) {
LOGGER.log(Level.INFO, "Write token into file {0}", path);
FileOutputStream fout = null;
ObjectOutputStream oos = null;
boolean res = false;
try {
fout = new FileOutputStream(path, false);
oos = new ObjectOutputStream(fout);
oos.writeObject(token);
res = true;
} catch (FileNotFoundException e) {
LOGGER.log(Level.SEVERE, "Cache file not found", e);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Error serializing object", e);
} finally {
IOUtils.closeQuietly(fout);
IOUtils.closeQuietly(oos);
}
return res;
}
private AccessToken getNewToken() throws AzureCloudException {
LOGGER.log(Level.INFO, "Retrieve new access token");
// reset configuration instance: renew token
Configuration.setInstance(null);
final ExecutorService service = Executors.newFixedThreadPool(1);
AuthenticationResult authres = null;
try {
LOGGER.log(Level.INFO, "Aquiring access token: \n\t{0}\n\t{1}\n\t{2}",
new Object[] { oauth2TokenEndpoint, serviceManagementURL, clientId });
final ClientCredential credential = new ClientCredential(clientId, clientSecret);
final Future<AuthenticationResult> future = new AuthenticationContext(oauth2TokenEndpoint, false, service).
acquireToken(serviceManagementURL, credential, null);
authres = future.get();
} catch (MalformedURLException e) {
throw new AzureCloudException("Authentication error", e);
} catch (InterruptedException e) {
throw new AzureCloudException("Authentication interrupted", e);
} catch (ExecutionException e) {
throw new AzureCloudException("Authentication execution failed", e);
} finally {
service.shutdown();
}
if (authres == null) {
throw new AzureCloudException("Authentication result was null");
}
LOGGER.log(Level.INFO,
"Authentication result:\n\taccess token: {0}\n\tExpires On: {1}",
new Object[] { authres.getAccessToken(), authres.getExpiresOnDate() });
final AccessToken token = new AccessToken(subscriptionId, serviceManagementURL, authres);
writeTokenFile(token);
return token;
}
}