Implements access token cache with file persistence
This commit is contained in:
parent
2c6bb7a711
commit
8ad20f7780
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user