Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SNOW-352846 OAuth Authentication: #2 OAuth Support #537

Merged
merged 13 commits into from
Jul 12, 2023

Conversation

sfc-gh-alhuang
Copy link
Contributor

@sfc-gh-alhuang sfc-gh-alhuang commented Jun 21, 2023

This PR includes core and test code for OAuth authentication. For more details, please refer to this doc.

According to the discussion, the integration would be added in dew. All OAuth unit test would be perform by mock OAuth client. For detail of integration test, please refer to this PR.

Copy link
Contributor

@sfc-gh-asen sfc-gh-asen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, had bunch of nit comments mostly

}

/** refreshToken - Get new access token using refresh_token, client_id, client_secret */
void refreshToken() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: it looks like this can be private

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

src/main/java/net/snowflake/ingest/utils/Utils.java Outdated Show resolved Hide resolved
@@ -279,6 +290,28 @@ public RequestBuilder(
this.host = hostName;
this.userAgentSuffix = userAgentSuffix;

// create our security/token manager
switch (credential.getClass().getSimpleName()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a getClass to do the type check is probably fine here but I'd rather we use instanceof check here instead. The main reason is that, getClass().getSimpleName() will break if someone creates their own credentials class but instanceof will work for inheritance without problem.


if (credential instanceof KeyPair) {
...
} else if( credential instanceof OAuthCredential) {
...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks for the tip!

}

/** Test instantiate OAuth manager with invalid params */
@Test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use
@test(expected = IllegalArgumentException.class)
for test cases throws the correct exception

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, thanks for the tip!

@codecov
Copy link

codecov bot commented Jun 22, 2023

Codecov Report

Merging #537 (fad5a00) into master (7d01fb7) will decrease coverage by 0.08%.
The diff coverage is 78.98%.

@@            Coverage Diff             @@
##           master     #537      +/-   ##
==========================================
- Coverage   78.37%   78.30%   -0.08%     
==========================================
  Files          73       76       +3     
  Lines        4597     4734     +137     
  Branches      408      424      +16     
==========================================
+ Hits         3603     3707     +104     
- Misses        821      846      +25     
- Partials      173      181       +8     
Impacted Files Coverage Δ
...va/net/snowflake/ingest/connection/JWTManager.java 86.04% <ø> (-4.66%) ⬇️
...ain/java/net/snowflake/ingest/utils/Constants.java 52.38% <ø> (ø)
...wflake/ingest/connection/SnowflakeOAuthClient.java 70.73% <70.73%> (ø)
...et/snowflake/ingest/connection/RequestBuilder.java 73.75% <72.72%> (-0.91%) ⬇️
.../net/snowflake/ingest/connection/OAuthManager.java 76.92% <76.92%> (ø)
...ternal/SnowflakeStreamingIngestClientInternal.java 86.92% <78.94%> (-1.60%) ⬇️
...rc/main/java/net/snowflake/ingest/utils/Utils.java 59.57% <86.66%> (+4.88%) ⬆️
...t/snowflake/ingest/connection/OAuthCredential.java 100.00% <100.00%> (ø)
...ain/java/net/snowflake/ingest/utils/ErrorCode.java 97.50% <100.00%> (+0.20%) ⬆️

... and 1 file with indirect coverage changes

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

Copy link
Contributor

@sfc-gh-tzhang sfc-gh-tzhang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice change! Left some comments, PTAL

@@ -0,0 +1,55 @@
/*
* Copyright (c) 2012-2023 Snowflake Computing Inc. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: 2023

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@Override
String getToken() {
if (refreshFailed.get()) {
LOGGER.error("getToken request failed due to token refresh failure");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which LOGGER are you using?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And do we need to log it since an exception will be thrown?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The LOGGER is from its super SecurityManager. Each client has exactly one SecurityManager which is either JWTManager or OAuthManager.

String getToken() {
if (refreshFailed.get()) {
LOGGER.error("getToken request failed due to token refresh failure");
throw new SecurityException();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a message in the exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved error message into the exception.

@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Snowflake Computing Inc. All rights reserved.
* Copyright (c) 2023 Snowflake Computing Inc. All rights reserved.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why updating the year?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated by mistake, fixed.

channelStatus.getChannelName(),
channelStatus.getChannelSequencer(),
channelStatus.getStatusCode(),
channelStatus.getMessage(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the message is removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed my mistake merge, reverted.

@@ -50,6 +56,7 @@ public class Constants {
public static final int MAX_STREAMING_INGEST_API_CHANNEL_RETRY = 3;
public static final int STREAMING_INGEST_TELEMETRY_UPLOAD_INTERVAL_IN_SEC = 10;
public static final long EP_NDV_UNKNOWN = -1L;
public static final int MAX_REFRESH_TOKEN_RETRY = 3;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this only used for OAuth? if yes, could you add it to the name to make it clear?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, modified.

this.refreshToken = refreshToken;
}

public void setExpires_in(int expires_in) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be expiresIn.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

private final String clientSecret;
private String accessToken;
private String refreshToken;
private int expires_in;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, expiresIn

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment on lines 319 to 333
/**
* RequestBuilder - this constructor is for testing OAuth purposes only
*
* @param securityManager - security manager, either JWTManager or OAuthManager
*/
public RequestBuilder(SecurityManager securityManager) {
this.port = DEFAULT_PORT;
this.scheme = DEFAULT_SCHEME;
this.host = DEFAULT_HOST_SUFFIX;
this.securityManager = securityManager;

this.userAgentSuffix = null;
this.authType = "OAuth";
this.telemetryService = null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This belongs in a test class

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible put a constructor of RequestBuilder in other class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just found out we actually don't need this constructor. Removed.

Comment on lines 65 to 66
// The authorization type used in auth header
public final String authType;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you infer this from the SecurityManager type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

securityManager =
new OAuthManager(
accountName, userName, (OAuthCredential) credential, makeBaseURI(), telemetryService);
authType = Constants.OAUTH;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are assigning securityManager here could we simply call something like securityManager.getAuthType() and use when needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out that we actually don't need authType, removed.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SnowflakeOAuthClient implements OAuthClient {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

Copy link
Contributor

@sfc-gh-tzhang sfc-gh-tzhang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Left some minor comments, PTAL. Could you make sure the Code Coverage report won't regress? Looks like we're missing some test coverage

throw new IllegalArgumentException();
}

// Set up the telemetry service if needed
this.telemetryService =
ENABLE_TELEMETRY_TO_SF
ENABLE_TELEMETRY_TO_SF && credential instanceof KeyPair
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a TODO in the comment with JIRA mentioning that OAuth support will come later

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new SFException(e, ErrorCode.KEYPAIR_CREATION_FAILURE);
}
logger.logInfo("Using JWT KeyPair for authorization");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could we combine the log line with the one below and use the authType as an input?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Comment on lines +40 to +42
MAKE_URI_FAILURE("0032"),
OAUTH_REFRESH_TOKEN_ERROR("0033"),
INVALID_CONFIG_PARAMETER("0034");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't the corresponding error messages being added, did I miss anything?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sfc-gh-lsembera Thanks for the hint! I'll send a PR to add the error messages.

OAuthCredential oAuthCredential,
URIBuilder baseURIBuilder,
TelemetryService telemetryService) {
this(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No tests

baseURIBuilder,
DEFAULT_UPDATE_THRESHOLD_RATIO,
telemetryService);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no tests?


// check if update threshold is within (0, 1)
if (updateThresholdRatio <= 0 || updateThresholdRatio >= 1) {
throw new IllegalArgumentException("updateThresholdRatio should fall in (0, 1)");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing tests

Comment on lines +94 to +95
refreshToken();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not covered by tests?

Copy link
Contributor Author

@sfc-gh-alhuang sfc-gh-alhuang Jul 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this #L94-L95 and #L50-L57, those lines require actual snowflake oauth client to run. The lines required backdoor function which cannot be test in this repo, the tests are added in the dew test PR.

Copy link
Contributor

@sfc-gh-tjones sfc-gh-tjones left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes since last time look good! See my comments about some missing tests. Thanks!

@sfc-gh-alhuang sfc-gh-alhuang merged commit 2999390 into master Jul 12, 2023
10 of 11 checks passed
@sfc-gh-alhuang sfc-gh-alhuang deleted the alhuang-oauth-2 branch July 12, 2023 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants