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
Conversation
...c/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt
Outdated
Show resolved
Hide resolved
|
||
private fun checkSafeLogging() { | ||
val piiTrueToCheck = listOf( | ||
"password:","password=", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not any()
?
There was a problem hiding this comment.
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()
This reverts commit 761a77f.
elementsToCheck.forEach { regex -> | ||
if (RegexMatcher(regex).matches(message)) { | ||
failCalled = true | ||
fail("PII $regex found in log message: $message") |
There was a problem hiding this comment.
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 thefailCalled
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 forspyLoggerCallback.failCalled
(to performassertFalse
), before the callback has finished and set the value totrue
. given that the default isfalse
, 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 atLoggerTest.java
. Another good way in general when dealing with callbacks vs. coroutines issuspendCancellableCoroutine
(this might not work here, but is interesting for you to look into).
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 -> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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)[:=]""" |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
...c/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt
Outdated
Show resolved
Hide resolved
...c/test/java/com/microsoft/identity/nativeauth/NativeAuthPublicClientApplicationKotlinTest.kt
Outdated
Show resolved
Hide resolved
this.allowPII = allowPII; | ||
} | ||
|
||
private final List<String> sensitivePIIMessages = Arrays.asList( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice and clean 👍
@@ -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}} |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Comments and Changes Summary:
@Before
and@After
.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 whenallowPII=false
but can exist whenallowPII=true
@ParameterizedRobolectricTestRunner.Parameters
in order to test bothallow=false & allowPII=true
forRunWith(ParameterizedRobolectricTestRunner::class) NativeAuthPublicClientApplicationKotlinTest(private val allowPII: Boolean)
verifyLogDoesNotContain
checksmessage
and 'containsPII'.verifyLogDoesNotContain
checksmessage
.