HADOOP-16933. Backport HADOOP-16890 and HADOOP-16825 to branch-2

Hadoop 16890. Change in expiry calculation for MSI token provider. Contributed by Bilahari T H
(cherry picked from commit 0b931f36ec)

HADOOP-16825: ITestAzureBlobFileSystemCheckAccess failing. Contributed by Bilahari T H
(cherry picked from commit 5944d28130)
This commit is contained in:
bilaharith 2020-04-07 08:40:14 +05:30 committed by Jonathan Hung
parent 3bc2e26cd7
commit 8f60971138
7 changed files with 199 additions and 19 deletions

View File

@ -1315,6 +1315,11 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>1.3</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@ -249,6 +249,12 @@
<artifactId>mockito-all</artifactId> <artifactId>mockito-all</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<profiles> <profiles>

View File

@ -72,7 +72,7 @@ public abstract class AccessTokenProvider {
* *
* @return true if the token is expiring in next 5 minutes * @return true if the token is expiring in next 5 minutes
*/ */
private boolean isTokenAboutToExpire() { protected boolean isTokenAboutToExpire() {
if (token == null) { if (token == null) {
LOG.debug("AADToken: no token. Returning expiring=true"); LOG.debug("AADToken: no token. Returning expiring=true");
return true; // no token should have same response as expired token return true; // no token should have same response as expired token

View File

@ -136,7 +136,7 @@ public final class AzureADAuthenticator {
headers.put("Metadata", "true"); headers.put("Metadata", "true");
LOG.debug("AADToken: starting to fetch token using MSI"); LOG.debug("AADToken: starting to fetch token using MSI");
return getTokenCall(authEndpoint, qp.serialize(), headers, "GET"); return getTokenCall(authEndpoint, qp.serialize(), headers, "GET", true);
} }
/** /**
@ -197,8 +197,13 @@ public final class AzureADAuthenticator {
} }
private static AzureADToken getTokenCall(String authEndpoint, String body, private static AzureADToken getTokenCall(String authEndpoint, String body,
Hashtable<String, String> headers, String httpMethod) Hashtable<String, String> headers, String httpMethod) throws IOException {
throws IOException { return getTokenCall(authEndpoint, body, headers, httpMethod, false);
}
private static AzureADToken getTokenCall(String authEndpoint, String body,
Hashtable<String, String> headers, String httpMethod, boolean isMsi)
throws IOException {
AzureADToken token = null; AzureADToken token = null;
ExponentialRetryPolicy retryPolicy ExponentialRetryPolicy retryPolicy
= new ExponentialRetryPolicy(3, 0, 1000, 2); = new ExponentialRetryPolicy(3, 0, 1000, 2);
@ -211,7 +216,7 @@ public final class AzureADAuthenticator {
httperror = 0; httperror = 0;
ex = null; ex = null;
try { try {
token = getTokenSingleCall(authEndpoint, body, headers, httpMethod); token = getTokenSingleCall(authEndpoint, body, headers, httpMethod, isMsi);
} catch (HttpException e) { } catch (HttpException e) {
httperror = e.httpErrorCode; httperror = e.httpErrorCode;
ex = e; ex = e;
@ -227,8 +232,9 @@ public final class AzureADAuthenticator {
return token; return token;
} }
private static AzureADToken getTokenSingleCall( private static AzureADToken getTokenSingleCall(String authEndpoint,
String authEndpoint, String payload, Hashtable<String, String> headers, String httpMethod) String payload, Hashtable<String, String> headers, String httpMethod,
boolean isMsi)
throws IOException { throws IOException {
AzureADToken token = null; AzureADToken token = null;
@ -268,7 +274,7 @@ public final class AzureADAuthenticator {
if (httpResponseCode == HttpURLConnection.HTTP_OK if (httpResponseCode == HttpURLConnection.HTTP_OK
&& responseContentType.startsWith("application/json") && responseContentLength > 0) { && responseContentType.startsWith("application/json") && responseContentLength > 0) {
InputStream httpResponseStream = conn.getInputStream(); InputStream httpResponseStream = conn.getInputStream();
token = parseTokenFromStream(httpResponseStream); token = parseTokenFromStream(httpResponseStream, isMsi);
} else { } else {
String responseBody = consumeInputStream(conn.getErrorStream(), 1024); String responseBody = consumeInputStream(conn.getErrorStream(), 1024);
String proxies = "none"; String proxies = "none";
@ -296,10 +302,12 @@ public final class AzureADAuthenticator {
return token; return token;
} }
private static AzureADToken parseTokenFromStream(InputStream httpResponseStream) throws IOException { private static AzureADToken parseTokenFromStream(
InputStream httpResponseStream, boolean isMsi) throws IOException {
AzureADToken token = new AzureADToken(); AzureADToken token = new AzureADToken();
try { try {
int expiryPeriod = 0; int expiryPeriodInSecs = 0;
long expiresOnInSecs = -1;
JsonFactory jf = new JsonFactory(); JsonFactory jf = new JsonFactory();
JsonParser jp = jf.createJsonParser(httpResponseStream); JsonParser jp = jf.createJsonParser(httpResponseStream);
@ -314,17 +322,38 @@ public final class AzureADAuthenticator {
if (fieldName.equals("access_token")) { if (fieldName.equals("access_token")) {
token.setAccessToken(fieldValue); token.setAccessToken(fieldValue);
} }
if (fieldName.equals("expires_in")) { if (fieldName.equals("expires_in")) {
expiryPeriod = Integer.parseInt(fieldValue); expiryPeriodInSecs = Integer.parseInt(fieldValue);
} }
if (fieldName.equals("expires_on")) {
expiresOnInSecs = Long.parseLong(fieldValue);
}
} }
jp.nextToken(); jp.nextToken();
} }
jp.close(); jp.close();
long expiry = System.currentTimeMillis(); if (expiresOnInSecs > 0) {
expiry = expiry + expiryPeriod * 1000L; // convert expiryPeriod to milliseconds and add LOG.debug("Expiry based on expires_on: {}", expiresOnInSecs);
token.setExpiry(new Date(expiry)); token.setExpiry(new Date(expiresOnInSecs * 1000));
LOG.debug("AADToken: fetched token with expiry " + token.getExpiry().toString()); } else {
if (isMsi) {
// Currently there is a known issue that MSI does not update expires_in
// for refresh and will have the value from first AAD token fetch request.
// Due to this known limitation, expires_in is not supported for MSI token fetch flow.
throw new UnsupportedOperationException("MSI Responded with invalid expires_on");
}
LOG.debug("Expiry based on expires_in: {}", expiryPeriodInSecs);
long expiry = System.currentTimeMillis();
expiry = expiry + expiryPeriodInSecs * 1000L; // convert expiryPeriod to milliseconds and add
token.setExpiry(new Date(expiry));
}
LOG.debug("AADToken: fetched token with expiry {}, expiresOn passed: {}",
token.getExpiry().toString(), expiresOnInSecs);
} catch (Exception ex) { } catch (Exception ex) {
LOG.debug("AADToken: got exception when parsing json token " + ex.toString()); LOG.debug("AADToken: got exception when parsing json token " + ex.toString());
throw ex; throw ex;

View File

@ -36,6 +36,10 @@ public class MsiTokenProvider extends AccessTokenProvider {
private final String clientId; private final String clientId;
private long tokenFetchTime = -1;
private static final long ONE_HOUR = 3600 * 1000;
private static final Logger LOG = LoggerFactory.getLogger(AccessTokenProvider.class); private static final Logger LOG = LoggerFactory.getLogger(AccessTokenProvider.class);
public MsiTokenProvider(final String authEndpoint, final String tenantGuid, public MsiTokenProvider(final String authEndpoint, final String tenantGuid,
@ -51,6 +55,36 @@ public class MsiTokenProvider extends AccessTokenProvider {
LOG.debug("AADToken: refreshing token from MSI"); LOG.debug("AADToken: refreshing token from MSI");
AzureADToken token = AzureADAuthenticator AzureADToken token = AzureADAuthenticator
.getTokenFromMsi(authEndpoint, tenantGuid, clientId, authority, false); .getTokenFromMsi(authEndpoint, tenantGuid, clientId, authority, false);
tokenFetchTime = System.currentTimeMillis();
return token; return token;
} }
/**
* Checks if the token is about to expire as per base expiry logic.
* Otherwise try to expire every 1 hour
*
* @return true if the token is expiring in next 1 hour or if a token has
* never been fetched
*/
@Override
protected boolean isTokenAboutToExpire() {
if (tokenFetchTime == -1 || super.isTokenAboutToExpire()) {
return true;
}
boolean expiring = false;
long elapsedTimeSinceLastTokenRefreshInMillis =
System.currentTimeMillis() - tokenFetchTime;
expiring = elapsedTimeSinceLastTokenRefreshInMillis >= ONE_HOUR
|| elapsedTimeSinceLastTokenRefreshInMillis < 0;
// In case of, Token is not refreshed for 1 hr or any clock skew issues,
// refresh token.
if (expiring) {
LOG.debug("MSIToken: token renewing. Time elapsed since last token fetch:"
+ " {} milli seconds", elapsedTimeSinceLastTokenRefreshInMillis);
}
return expiring;
}
} }

View File

@ -0,0 +1,93 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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 org.apache.hadoop.fs.azurebfs;
import java.io.IOException;
import java.util.Date;
import org.junit.Test;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.fs.azurebfs.oauth2.AccessTokenProvider;
import org.apache.hadoop.fs.azurebfs.oauth2.AzureADToken;
import org.apache.hadoop.fs.azurebfs.oauth2.MsiTokenProvider;
import static org.junit.Assume.assumeThat;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.isEmptyOrNullString;
import static org.hamcrest.Matchers.isEmptyString;
import static org.apache.hadoop.fs.azurebfs.constants.AuthConfigurations.DEFAULT_FS_AZURE_ACCOUNT_OAUTH_MSI_AUTHORITY;
import static org.apache.hadoop.fs.azurebfs.constants.AuthConfigurations.DEFAULT_FS_AZURE_ACCOUNT_OAUTH_MSI_ENDPOINT;
import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID;
import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_OAUTH_MSI_AUTHORITY;
import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_OAUTH_MSI_ENDPOINT;
import static org.apache.hadoop.fs.azurebfs.constants.ConfigurationKeys.FS_AZURE_ACCOUNT_OAUTH_MSI_TENANT;
/**
* Test MsiTokenProvider.
*/
public final class ITestAbfsMsiTokenProvider
extends AbstractAbfsIntegrationTest {
public ITestAbfsMsiTokenProvider() throws Exception {
super();
}
@Test
public void test() throws IOException {
AbfsConfiguration conf = getConfiguration();
assumeThat(conf.get(FS_AZURE_ACCOUNT_OAUTH_MSI_ENDPOINT),
not(isEmptyOrNullString()));
assumeThat(conf.get(FS_AZURE_ACCOUNT_OAUTH_MSI_TENANT),
not(isEmptyOrNullString()));
assumeThat(conf.get(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID),
not(isEmptyOrNullString()));
assumeThat(conf.get(FS_AZURE_ACCOUNT_OAUTH_MSI_AUTHORITY),
not(isEmptyOrNullString()));
String tenantGuid = conf
.getPasswordString(FS_AZURE_ACCOUNT_OAUTH_MSI_TENANT);
String clientId = conf.getPasswordString(FS_AZURE_ACCOUNT_OAUTH_CLIENT_ID);
String authEndpoint = getTrimmedPasswordString(conf,
FS_AZURE_ACCOUNT_OAUTH_MSI_ENDPOINT,
DEFAULT_FS_AZURE_ACCOUNT_OAUTH_MSI_ENDPOINT);
String authority = getTrimmedPasswordString(conf,
FS_AZURE_ACCOUNT_OAUTH_MSI_AUTHORITY,
DEFAULT_FS_AZURE_ACCOUNT_OAUTH_MSI_AUTHORITY);
AccessTokenProvider tokenProvider = new MsiTokenProvider(authEndpoint,
tenantGuid, clientId, authority);
AzureADToken token = null;
token = tokenProvider.getToken();
assertThat(token.getAccessToken(), not(isEmptyString()));
assertThat(token.getExpiry().after(new Date()), is(true));
}
private String getTrimmedPasswordString(AbfsConfiguration conf, String key,
String defaultValue) throws IOException {
String value = conf.getPasswordString(key);
if (StringUtils.isBlank(value)) {
value = defaultValue;
}
return value.trim();
}
}

View File

@ -54,7 +54,7 @@ public class ITestAzureBlobFileSystemCheckAccess
private static final String TEST_FOLDER_PATH = "CheckAccessTestFolder"; private static final String TEST_FOLDER_PATH = "CheckAccessTestFolder";
private final FileSystem superUserFs; private final FileSystem superUserFs;
private final FileSystem testUserFs; private FileSystem testUserFs;
private final String testUserGuid; private final String testUserGuid;
private final boolean isCheckAccessEnabled; private final boolean isCheckAccessEnabled;
private final boolean isHNSEnabled; private final boolean isHNSEnabled;
@ -64,13 +64,15 @@ public class ITestAzureBlobFileSystemCheckAccess
this.superUserFs = getFileSystem(); this.superUserFs = getFileSystem();
testUserGuid = getConfiguration() testUserGuid = getConfiguration()
.get(FS_AZURE_BLOB_FS_CHECKACCESS_TEST_USER_GUID); .get(FS_AZURE_BLOB_FS_CHECKACCESS_TEST_USER_GUID);
this.testUserFs = getTestUserFs();
this.isCheckAccessEnabled = getConfiguration().isCheckAccessEnabled(); this.isCheckAccessEnabled = getConfiguration().isCheckAccessEnabled();
this.isHNSEnabled = getConfiguration() this.isHNSEnabled = getConfiguration()
.getBoolean(FS_AZURE_TEST_NAMESPACE_ENABLED_ACCOUNT, false); .getBoolean(FS_AZURE_TEST_NAMESPACE_ENABLED_ACCOUNT, false);
} }
private FileSystem getTestUserFs() throws Exception { private void setTestUserFs() throws Exception {
if (this.testUserFs != null) {
return;
}
String orgClientId = getConfiguration().get(FS_AZURE_BLOB_FS_CLIENT_ID); String orgClientId = getConfiguration().get(FS_AZURE_BLOB_FS_CLIENT_ID);
String orgClientSecret = getConfiguration() String orgClientSecret = getConfiguration()
.get(FS_AZURE_BLOB_FS_CLIENT_SECRET); .get(FS_AZURE_BLOB_FS_CLIENT_SECRET);
@ -89,7 +91,7 @@ public class ITestAzureBlobFileSystemCheckAccess
getRawConfiguration() getRawConfiguration()
.setBoolean(AZURE_CREATE_REMOTE_FILESYSTEM_DURING_INITIALIZATION, .setBoolean(AZURE_CREATE_REMOTE_FILESYSTEM_DURING_INITIALIZATION,
orgCreateFileSystemDurungInit); orgCreateFileSystemDurungInit);
return fs; this.testUserFs = fs;
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
@ -112,6 +114,7 @@ public class ITestAzureBlobFileSystemCheckAccess
@Test(expected = FileNotFoundException.class) @Test(expected = FileNotFoundException.class)
public void testCheckAccessForNonExistentFile() throws Exception { public void testCheckAccessForNonExistentFile() throws Exception {
assumeHNSAndCheckAccessEnabled(); assumeHNSAndCheckAccessEnabled();
setTestUserFs();
Path nonExistentFile = setupTestDirectoryAndUserAccess( Path nonExistentFile = setupTestDirectoryAndUserAccess(
"/nonExistentFile1.txt", FsAction.ALL); "/nonExistentFile1.txt", FsAction.ALL);
superUserFs.delete(nonExistentFile, true); superUserFs.delete(nonExistentFile, true);
@ -155,12 +158,16 @@ public class ITestAzureBlobFileSystemCheckAccess
Assume.assumeFalse(FS_AZURE_TEST_NAMESPACE_ENABLED_ACCOUNT + " is true", Assume.assumeFalse(FS_AZURE_TEST_NAMESPACE_ENABLED_ACCOUNT + " is true",
getConfiguration() getConfiguration()
.getBoolean(FS_AZURE_TEST_NAMESPACE_ENABLED_ACCOUNT, true)); .getBoolean(FS_AZURE_TEST_NAMESPACE_ENABLED_ACCOUNT, true));
Assume.assumeTrue(FS_AZURE_ENABLE_CHECK_ACCESS + " is false",
isCheckAccessEnabled);
setTestUserFs();
testUserFs.access(new Path("/"), FsAction.READ); testUserFs.access(new Path("/"), FsAction.READ);
} }
@Test @Test
public void testFsActionNONE() throws Exception { public void testFsActionNONE() throws Exception {
assumeHNSAndCheckAccessEnabled(); assumeHNSAndCheckAccessEnabled();
setTestUserFs();
Path testFilePath = setupTestDirectoryAndUserAccess("/test2.txt", Path testFilePath = setupTestDirectoryAndUserAccess("/test2.txt",
FsAction.NONE); FsAction.NONE);
assertInaccessible(testFilePath, FsAction.EXECUTE); assertInaccessible(testFilePath, FsAction.EXECUTE);
@ -175,6 +182,7 @@ public class ITestAzureBlobFileSystemCheckAccess
@Test @Test
public void testFsActionEXECUTE() throws Exception { public void testFsActionEXECUTE() throws Exception {
assumeHNSAndCheckAccessEnabled(); assumeHNSAndCheckAccessEnabled();
setTestUserFs();
Path testFilePath = setupTestDirectoryAndUserAccess("/test3.txt", Path testFilePath = setupTestDirectoryAndUserAccess("/test3.txt",
FsAction.EXECUTE); FsAction.EXECUTE);
assertAccessible(testFilePath, FsAction.EXECUTE); assertAccessible(testFilePath, FsAction.EXECUTE);
@ -190,6 +198,7 @@ public class ITestAzureBlobFileSystemCheckAccess
@Test @Test
public void testFsActionREAD() throws Exception { public void testFsActionREAD() throws Exception {
assumeHNSAndCheckAccessEnabled(); assumeHNSAndCheckAccessEnabled();
setTestUserFs();
Path testFilePath = setupTestDirectoryAndUserAccess("/test4.txt", Path testFilePath = setupTestDirectoryAndUserAccess("/test4.txt",
FsAction.READ); FsAction.READ);
assertAccessible(testFilePath, FsAction.READ); assertAccessible(testFilePath, FsAction.READ);
@ -205,6 +214,7 @@ public class ITestAzureBlobFileSystemCheckAccess
@Test @Test
public void testFsActionWRITE() throws Exception { public void testFsActionWRITE() throws Exception {
assumeHNSAndCheckAccessEnabled(); assumeHNSAndCheckAccessEnabled();
setTestUserFs();
Path testFilePath = setupTestDirectoryAndUserAccess("/test5.txt", Path testFilePath = setupTestDirectoryAndUserAccess("/test5.txt",
FsAction.WRITE); FsAction.WRITE);
assertAccessible(testFilePath, FsAction.WRITE); assertAccessible(testFilePath, FsAction.WRITE);
@ -220,6 +230,7 @@ public class ITestAzureBlobFileSystemCheckAccess
@Test @Test
public void testFsActionREADEXECUTE() throws Exception { public void testFsActionREADEXECUTE() throws Exception {
assumeHNSAndCheckAccessEnabled(); assumeHNSAndCheckAccessEnabled();
setTestUserFs();
Path testFilePath = setupTestDirectoryAndUserAccess("/test6.txt", Path testFilePath = setupTestDirectoryAndUserAccess("/test6.txt",
FsAction.READ_EXECUTE); FsAction.READ_EXECUTE);
assertAccessible(testFilePath, FsAction.EXECUTE); assertAccessible(testFilePath, FsAction.EXECUTE);
@ -235,6 +246,7 @@ public class ITestAzureBlobFileSystemCheckAccess
@Test @Test
public void testFsActionWRITEEXECUTE() throws Exception { public void testFsActionWRITEEXECUTE() throws Exception {
assumeHNSAndCheckAccessEnabled(); assumeHNSAndCheckAccessEnabled();
setTestUserFs();
Path testFilePath = setupTestDirectoryAndUserAccess("/test7.txt", Path testFilePath = setupTestDirectoryAndUserAccess("/test7.txt",
FsAction.WRITE_EXECUTE); FsAction.WRITE_EXECUTE);
assertAccessible(testFilePath, FsAction.EXECUTE); assertAccessible(testFilePath, FsAction.EXECUTE);
@ -250,6 +262,7 @@ public class ITestAzureBlobFileSystemCheckAccess
@Test @Test
public void testFsActionALL() throws Exception { public void testFsActionALL() throws Exception {
assumeHNSAndCheckAccessEnabled(); assumeHNSAndCheckAccessEnabled();
setTestUserFs();
Path testFilePath = setupTestDirectoryAndUserAccess("/test8.txt", Path testFilePath = setupTestDirectoryAndUserAccess("/test8.txt",
FsAction.ALL); FsAction.ALL);
assertAccessible(testFilePath, FsAction.EXECUTE); assertAccessible(testFilePath, FsAction.EXECUTE);