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

Native auth safe logging test #2092

Merged
merged 39 commits into from May 14, 2024
Merged

Native auth safe logging test #2092

merged 39 commits into from May 14, 2024

Conversation

Yuki-YuXin
Copy link
Contributor

@Yuki-YuXin Yuki-YuXin commented Apr 29, 2024

Comments and Changes Summary:

  • Setup external logger and remove it for every test under @Before and @After.
  • Use Regex rules to filter the log message under verify().log(). There are two pools for the regex rules: sensitivePIIMessages - items should not exist in the log regardless of allowPII value; permittedPIIMessages - items should not exist in the log when allowPII=false but can exist when allowPII=true
  • Use @ParameterizedRobolectricTestRunner.Parameters in order to test both allow=false & allowPII=true for RunWith(ParameterizedRobolectricTestRunner::class) NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean)
  • verifyLogDoesNotContain checks message and 'containsPII'. verifyLogDoesNotContain checks message.

@github-actions github-actions bot added the msal label Apr 29, 2024

private fun checkSafeLogging() {
val piiTrueToCheck = listOf(
"password:","password=",
Copy link
Contributor

@SammyO SammyO Apr 29, 2024

Choose a reason for hiding this comment

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

let's make this list longer.

  • "password" (so without : and =)
  • '"password"' (so looking for the password value that includes " in the string, for example when JSON is outputted it will contain ")
  • the above 2 points also for code and username
  • attributes is another PII field
  • channel target label is another
  • add check for access token, ID token and refresh token
  • add snake_case as well as camelCase for multi-word values like continuation token.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have updated the rules and scopes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should (user) attributes be added?

Logger.getInstance().setExternalLogger(null)
}
private fun verifyLogDoesNotContain(tag: String, element: String) {
verify(externalLogger, never()).log(contains(tag), any(), contains(element), eq(true))
Copy link
Contributor

@SammyO SammyO Apr 29, 2024

Choose a reason for hiding this comment

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

I think containsPII should be any(). Without it, I believe Mockito is looking for something like:
-> assert that the callback is never called with values tag and and and containsPII=true.
whereas what might be happening is that is called, but PII=false. If that happens, it won't trigger this I think.

Furthermore, you're actually doing many tests in 1 statement here. While it's efficient from a coding perspective, it may not be logistically. if this test fails, it's hard to determine why, because so many things happen in the same statement.

Consider something like:

  • attach a real logger callback interface (make it a Spy, so the method is actually called, but you can also play around with its behaviour the way you can with a Mock)
  • within the callback, perform a number of assertions all on different fields and callback configurations, each in a separate statement.

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 according to the suggestions.

Logger.getInstance().setExternalLogger(null)
}
private fun verifyLogDoesNotContain(tag: String, element: String) {
verify(externalLogger, never()).log(contains(tag), any(), contains(element), eq(true))
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe we want to do 2 types of checks:

  • some values should never be in the callback
  • some values may be, but only if also containsPII=true (and if the log configuration has allowPII enabled)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Modify from eq(true) to any(). However, the value of allowPII is defined variable of true status.

}

elementsToCheck.forEach { element ->
verifyLogDoesNotContain("Command", element)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we only care about TAG=Command? I think we should make the tag matcher any(), as we don't care what the tag value is in this context.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Narrowing down the tag contributes to easier regex definition. Modify from "Command|Indicator" to any()

"(?i)access_token|\baccessToken(?![:=])\b", // access_token, accessToken
"(?i)refresh_token|\brefreshToken(?![:=])\b",
"(?i)id_token|\bidToken(?![:=])\b",
"(?i)client_secret|clientSecret",
Copy link
Contributor

Choose a reason for hiding this comment

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

do we need client secret?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, we currently don't. Removed.

}

elementsToCheck.forEach { regex ->
verifyLogDoesNotContain("""\S*(Command|Interactor)\S*""", regex)
Copy link
Contributor

Choose a reason for hiding this comment

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

why not any()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Modify from regex to any()

@Yuki-YuXin Yuki-YuXin marked this pull request as ready for review April 30, 2024 08:56
@Yuki-YuXin Yuki-YuXin requested review from a team as code owners April 30, 2024 08:56
elementsToCheck.forEach { regex ->
if (RegexMatcher(regex).matches(message)) {
failCalled = true
fail("PII $regex found in log message: $message")
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a smart setup, but I'm 100% sure it's going to work. Some thoughts:

  • I see we can't rely on only fail, as this callback is executed on a thread that's different from the thread that the test is executed on. I see that's why you've introduced the failCalled check, that's smart.
  • however, given the fact that failCalled is set to true on a different thread, I expect there might be race conditions where the test thread is looking for spyLoggerCallback.failCalled (to perform assertFalse), before the callback has finished and set the value to true. given that the default is false, that means the test "thinks" failCalled wasn't set. Whereas in reality it hasn't been set yet
  • you can try this out by adding a delay to the logger callback (e.g. Thread.sleep(5000)).
  • this setup will cause flaky tests: sometimes they'll pass, sometimes they'll fail. They're not deterministic, due to the race condition.
  • typically a good way to deal with these async processes in tests is through for example CountDownLatch. Take a look at LoggerTest.java. Another good way in general when dealing with callbacks vs. coroutines is suspendCancellableCoroutine (this might not work here, but is interesting for you to look into).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks a million for this valuable guidance. Have added CountDownLatch into the loggercallback. But I just make changes for scenario 1 for reviewing first.

@@ -101,6 +107,46 @@ class NativeAuthPublicClientApplicationKotlinTest : PublicClientApplicationAbstr
private val password = "verySafePassword".toCharArray()
private val code = "1234"
private val emptyString = ""
private val allowPII = true
Copy link
Contributor

Choose a reason for hiding this comment

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

We will need tests for both true and false, right? I.e. in theory, every use case (test) should be performed with both PII enabled and disabled. Brute force way would be to duplicate every test (perhaps using a parameterised JUnit setup, e.g. see CommandResultUtilTest). Not ideal. Can we think of a smarter way to do this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Use Parameterized for this.

}
}
}
private val spyLoggerCallback = spy(loggerCallback)
Copy link
Contributor

Choose a reason for hiding this comment

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

it's actually not necessary to make this a spy I think.

containsPII: Boolean
) {
if (allowPII) {
allowPIIFalseToCheck.forEach { regex ->
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This part might be not necessary because LoggerTest takes the responsibility of validating containsPII.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not exactly. LoggerTest validates whether, if a native auth object states that it contains PII (through ILoggable), the callback's containsPII is set to true (and vice versa). But there are more ways through which containsPII can be set. Moreover: these tests validate whether containsPII is correctly set according to actual fields (like username, attributes, etc.), and they're run on a wide range of scenarios. This is more of an E2E test, whereas LoggerTest is a unit test that tests the ILoggable interface specifically.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Add verifyLogCouldContain function to validate whether containsPII is correctly set according to actual fields.

@@ -101,10 +112,31 @@ class NativeAuthPublicClientApplicationKotlinTest : PublicClientApplicationAbstr
private val password = "verySafePassword".toCharArray()
private val code = "1234"
private val emptyString = ""
private val infoPIIToCheck = listOf(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: why is this variable called info?

Copy link
Contributor Author

@Yuki-YuXin Yuki-YuXin May 8, 2024

Choose a reason for hiding this comment

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

Short for information. Renamed as sensitivePIIMessages as Robert suggested.

private val allowPIIFalseToCheck = listOf(
"""(?<![\(])username[:=]""",
"""(?i)\b(challengeTargetLabel|challenge_target_label)[:=]""",
"""(?i)\b(grantType|grant_type)[:=]"""
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO grant type is not PII. It's a developer setting, just like authority URL.

@@ -181,8 +221,21 @@ class NativeAuthPublicClientApplicationKotlinTest : PublicClientApplicationAbstr
MockApiResponseType.TOKEN_SUCCESS
)

val loggingNumber = if (allowPII) 137 else 127
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this used for? Did you manually count the amount of logs that are produced in a certain flow, and you're counting them down? If so, let's think of an alternative strategy as this is not scalable. This test would need to be updated very frequently, and the means to get to this count (i.e. manually counting logs) is time consuming.

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, counting first then counting down then. Have updated according to the offline discussion.

this.allowPII = allowPII;
}

private final List<String> sensitivePIIMessages = Arrays.asList(
Copy link
Contributor

Choose a reason for hiding this comment

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

can we add one more item here? A regex for a JWT, which would cover access tokens and ID tokens. For that regex you can omit the = and : parts.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. a JWT token or similar structure."""^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$""" // JWT token

checkSafeLogging();
}

private void checkSafeLogging() {
Copy link
Contributor

@SammyO SammyO May 10, 2024

Choose a reason for hiding this comment

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

Since this is used in both tests, and potentially more tests moving forward: can this be extracted into a dedicated component, so that reusability becomes more straight forward? Along with the permittedPIIMessages and sensitivePIIMessages lists. So that we have 1 source of truth.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Create a component called as LoggerCheckHelper which has all logger related stuff inside.

disableList.addAll(permittedPIIMessages);
}

for (String regex : allowList) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nice and clean 👍

@Yuki-YuXin Yuki-YuXin added the No-Changelog This change does not update the changelog. label May 10, 2024
@@ -66,7 +67,7 @@ jobs:
- task: Gradle@2
displayName: Run Unit tests
inputs:
tasks: msal:testLocalDebugUnitTest -Plabtest -PlabSecret=$(LabVaultAppSecret) -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}}
tasks: msal:testLocalDebugUnitTest -Plabtest -PlabSecret=$(LabVaultAppCert) -ProbolectricSdkVersion=${{variables.robolectricSdkVersion}}
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure about this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is opening PR (and has not merged) from MSAL to update the expired secret. I include their changes to see if that's the reason for the failed pipeline verification. I personally think we need to wait that PR to be merged in order to merge our work.

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree. Let's not introduce changes with this PR that don't belong here. Let's mark this PR as "draft" in the meantime.

@Yuki-YuXin Yuki-YuXin merged commit c207fcc into dev May 14, 2024
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
msal No-Changelog This change does not update the changelog.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants