diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientAllowAttesterSearchOnlyForDomainsEmptyFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientAllowAttesterSearchOnlyForDomainsEmptyFlowTest.kt new file mode 100644 index 0000000000..2752cbce37 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientAllowAttesterSearchOnlyForDomainsEmptyFlowTest.kt @@ -0,0 +1,91 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ +package com.flowcrypt.email.ui + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.clearText +import androidx.test.espresso.action.ViewActions.pressImeActionButton +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.activityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.flowcrypt.email.R +import com.flowcrypt.email.api.retrofit.response.model.OrgRules +import com.flowcrypt.email.base.BaseTest +import com.flowcrypt.email.rules.AddAccountToDatabaseRule +import com.flowcrypt.email.rules.ClearAppSettingsRule +import com.flowcrypt.email.rules.RetryRule +import com.flowcrypt.email.rules.ScreenshotTestRule +import com.flowcrypt.email.ui.activity.MainActivity +import com.flowcrypt.email.util.AccountDaoManager +import com.flowcrypt.email.util.TestGeneralUtil +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +/** + * @author Denis Bondarenko + * Date: 6/7/22 + * Time: 10:24 AM + * E-mail: DenBond7@gmail.com + */ +@MediumTest +@RunWith(AndroidJUnit4::class) +class Test : BaseTest() { + private val userWithOrgRules = AccountDaoManager.getUserWithOrgRules( + OrgRules( + flags = listOf( + OrgRules.DomainRule.NO_PRV_CREATE, + OrgRules.DomainRule.NO_PRV_BACKUP + ), + customKeyserverUrl = null, + keyManagerUrl = "https://keymanagerurl.test", + disallowAttesterSearchForDomains = listOf("*"), + allowAttesterSearchOnlyForDomains = listOf(), + enforceKeygenAlgo = null, + enforceKeygenExpireMonths = null + ) + ) + + private val addAccountToDatabaseRule = AddAccountToDatabaseRule(userWithOrgRules) + + override val useIntents: Boolean = true + override val activityScenarioRule = activityScenarioRule( + TestGeneralUtil.genIntentForNavigationComponent( + destinationId = R.id.importRecipientsFromSourceFragment + ) + ) + + @get:Rule + var ruleChain: TestRule = RuleChain + .outerRule(RetryRule.DEFAULT) + .around(ClearAppSettingsRule()) + .around(addAccountToDatabaseRule) + .around(activityScenarioRule) + .around(ScreenshotTestRule()) + + @Test + fun testEmptyAllowAttesterSearchOnlyForDomains() { + onView(withId(R.id.eTKeyIdOrEmail)) + .perform( + clearText(), + typeText("user@$SOME_DOMAIN"), + pressImeActionButton() + ) + + onView(withText(R.string.supported_public_key_not_found)) + .check(matches((isDisplayed()))) + } + + companion object { + private const val SOME_DOMAIN = "some.test" + } +} diff --git a/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientAllowAttesterSearchOnlyForDomainsFlowTest.kt b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientAllowAttesterSearchOnlyForDomainsFlowTest.kt new file mode 100644 index 0000000000..f6055bd3b5 --- /dev/null +++ b/FlowCrypt/src/androidTest/java/com/flowcrypt/email/ui/ImportRecipientAllowAttesterSearchOnlyForDomainsFlowTest.kt @@ -0,0 +1,150 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: DenBond7 + */ + +package com.flowcrypt.email.ui + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.clearText +import androidx.test.espresso.action.ViewActions.pressImeActionButton +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.activityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.MediumTest +import com.flowcrypt.email.R +import com.flowcrypt.email.TestConstants +import com.flowcrypt.email.api.retrofit.response.model.OrgRules +import com.flowcrypt.email.base.BaseTest +import com.flowcrypt.email.rules.AddAccountToDatabaseRule +import com.flowcrypt.email.rules.ClearAppSettingsRule +import com.flowcrypt.email.rules.FlowCryptMockWebServerRule +import com.flowcrypt.email.rules.RetryRule +import com.flowcrypt.email.rules.ScreenshotTestRule +import com.flowcrypt.email.ui.activity.MainActivity +import com.flowcrypt.email.util.AccountDaoManager +import com.flowcrypt.email.util.TestGeneralUtil +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.RecordedRequest +import org.junit.ClassRule +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import java.net.HttpURLConnection + +/** + * @author Denis Bondarenko + * Date: 6/6/22 + * Time: 4:30 PM + * E-mail: DenBond7@gmail.com + */ +@MediumTest +@RunWith(AndroidJUnit4::class) +class ImportRecipientAllowAttesterSearchOnlyForDomainsFlowTest : BaseTest() { + private val userWithOrgRules = AccountDaoManager.getUserWithOrgRules( + OrgRules( + flags = listOf( + OrgRules.DomainRule.NO_PRV_CREATE, + OrgRules.DomainRule.NO_PRV_BACKUP + ), + customKeyserverUrl = null, + keyManagerUrl = "https://keymanagerurl.test", + disallowAttesterSearchForDomains = listOf("*"), + allowAttesterSearchOnlyForDomains = listOf(ALLOWED_DOMAIN), + enforceKeygenAlgo = null, + enforceKeygenExpireMonths = null + ) + ) + + private val addAccountToDatabaseRule = AddAccountToDatabaseRule(userWithOrgRules) + + override val useIntents: Boolean = true + override val activityScenarioRule = activityScenarioRule( + TestGeneralUtil.genIntentForNavigationComponent( + destinationId = R.id.importRecipientsFromSourceFragment + ) + ) + + @get:Rule + var ruleChain: TestRule = RuleChain + .outerRule(RetryRule.DEFAULT) + .around(ClearAppSettingsRule()) + .around(addAccountToDatabaseRule) + .around(activityScenarioRule) + .around(ScreenshotTestRule()) + + @Test + fun testDisallowLookupOnAttester() { + onView(withId(R.id.eTKeyIdOrEmail)) + .perform( + clearText(), + typeText("user@$SOME_DOMAIN"), + pressImeActionButton() + ) + + onView(withText(R.string.supported_public_key_not_found)) + .check(matches((isDisplayed()))) + } + + @Test + fun testAllowAttesterSearchOnlyForDomains() { + onView(withId(R.id.eTKeyIdOrEmail)) + .perform( + clearText(), + typeText(ALLOWED_USER), + pressImeActionButton() + ) + + onView(withText(getResString(R.string.template_message_part_public_key_owner, ALLOWED_USER))) + .check(matches(isDisplayed())) + } + + companion object { + private const val SOME_DOMAIN = "some.test" + private const val ALLOWED_DOMAIN = "allowed.test" + private const val ALLOWED_USER = "user@$ALLOWED_DOMAIN" + private val ALLOWED_USER_PUB_KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: PGPainless\n" + + "\n" + + "mDMEYp4ILRYJKwYBBAHaRw8BAQdAbR/lV1cjANNrV3G6xKBSZvvUvSV/7tkAijfu\n" + + "Ic1wkPG0EXVzZXJAYWxsb3dlZC50ZXN0iI8EExYKAEEFAmKeCC0JEBqKFwXMKQ1t\n" + + "FiEEdezAhiRDeNnKk9VbGooXBcwpDW0CngECmwMFFgIDAQAECwkIBwUVCgkICwKZ\n" + + "AQAAMwcA/i5VkgbmbH/KQg95FplXdbS0LlIThWU/9a5niUDjLBw/AP4zbzcHmN/9\n" + + "pyLsGVej1vogLKHWvOoMhw/cj2z9OQfiCrg4BGKeCC0SCisGAQQBl1UBBQEBB0Bj\n" + + "ooQjx+z6b71CWOZewG0LSFGY13IuLmViuzOwAgXpUgMBCAeIdQQYFgoAHQUCYp4I\n" + + "LQKeAQKbDAUWAgMBAAQLCQgHBRUKCQgLAAoJEBqKFwXMKQ1tWrsA/RaP0ZVXxVoQ\n" + + "lXF9Qd24bBc63Y1F0sSEQxFKJgOCDJ3yAP4uDyqiUbVy8munXvxaBpMhxpqZx1B3\n" + + "bo7D52MQznBWBQ==\n" + + "=MoFp\n" + + "-----END PGP PUBLIC KEY BLOCK-----" + + @get:ClassRule + @JvmStatic + val mockWebServerRule = FlowCryptMockWebServerRule( + TestConstants.MOCK_WEB_SERVER_PORT, + object : Dispatcher() { + override fun dispatch(request: RecordedRequest): MockResponse { + if (request.path?.startsWith("/pub", ignoreCase = true) == true) { + val lastSegment = request.requestUrl?.pathSegments?.lastOrNull() + + when { + ALLOWED_USER.equals(lastSegment, true) -> { + return MockResponse() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setBody(ALLOWED_USER_PUB_KEY) + } + } + } + + return MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND) + } + }) + } +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiRepository.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiRepository.kt index d58495c35d..63118b796c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiRepository.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/ApiRepository.kt @@ -85,14 +85,14 @@ interface ApiRepository : BaseApiRepository { /** * @param requestCode A unique request code for this call - * @param context Interface to global information about an application environment. - * @param identData A key id or the user email or a fingerprint. - * @param orgRules Contains client configurations. + * @param context Interface to global information about an application environment. + * @param email A user email. + * @param orgRules Contains client configurations. */ suspend fun pubLookup( requestCode: Long = 0L, context: Context, - identData: String, + email: String, orgRules: OrgRules? = null ): Result diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt index c5274ee6b4..eb2a8c864d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/FlowcryptApiRepository.kt @@ -112,7 +112,7 @@ class FlowcryptApiRepository : ApiRepository { override suspend fun pubLookup( requestCode: Long, context: Context, - identData: String, + email: String, orgRules: OrgRules? ): Result = withContext(Dispatchers.IO) { @@ -139,9 +139,9 @@ class FlowcryptApiRepository : ApiRepository { } } - if (identData.isValidEmail()) { + if (email.isValidEmail()) { val wkdResult = getResult(requestCode = requestCode) { - val pgpPublicKeyRingCollection = WkdClient.lookupEmail(context, identData) + val pgpPublicKeyRingCollection = WkdClient.lookupEmail(context, email) //For now, we just peak at the first matching key. It should be improved inthe future. // See more details here https://github.com/FlowCrypt/flowcrypt-android/issues/480 @@ -159,21 +159,19 @@ class FlowcryptApiRepository : ApiRepository { return@withContext resultWrapperFun(wkdResult) } - if (orgRules?.canLookupThisRecipientOnAttester(identData) == false) { + if (orgRules?.canLookupThisRecipientOnAttester(email) == false) { return@withContext Result.success( requestCode = requestCode, data = PubResponse(null, null) ) } - } else if (orgRules?.disallowLookupOnAttester() == true) { - return@withContext Result.success( - requestCode = requestCode, - data = PubResponse(null, null) - ) - } + } else return@withContext Result.exception( + requestCode = requestCode, + throwable = IllegalStateException(context.getString(R.string.error_email_is_not_valid)) + ) val apiService = ApiHelper.getInstance(context).retrofit.create(ApiService::class.java) - val result = getResult(requestCode = requestCode) { apiService.getPubFromAttester(identData) } + val result = getResult(requestCode = requestCode) { apiService.getPubFromAttester(email) } return@withContext resultWrapperFun(result) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/OrgRules.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/OrgRules.kt index 71791daaf8..a6fdad1e7a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/OrgRules.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/retrofit/response/model/OrgRules.kt @@ -25,6 +25,8 @@ data class OrgRules constructor( @Expose val keyManagerUrl: String? = null, @SerializedName("disallow_attester_search_for_domains") @Expose val disallowAttesterSearchForDomains: List? = null, + @SerializedName("allow_attester_search_only_for_domains") + @Expose val allowAttesterSearchOnlyForDomains: List? = null, @SerializedName("enforce_keygen_algo") @Expose val enforceKeygenAlgo: KeyAlgo? = null, @SerializedName("enforce_keygen_expire_months") @@ -36,6 +38,7 @@ data class OrgRules constructor( parcel.readString(), parcel.readString(), parcel.createStringArrayList(), + parcel.createStringArrayList(), parcel.readParcelable(KeyAlgo::class.java.classLoader), parcel.readValue(Int::class.java.classLoader) as? Int ) @@ -45,6 +48,7 @@ data class OrgRules constructor( parcel.writeString(customKeyserverUrl) parcel.writeString(keyManagerUrl) parcel.writeStringList(disallowAttesterSearchForDomains) + parcel.writeStringList(allowAttesterSearchOnlyForDomains) parcel.writeParcelable(enforceKeygenAlgo, flagsList) parcel.writeValue(enforceKeygenExpireMonths) } @@ -179,30 +183,25 @@ data class OrgRules constructor( } /** - * Some orgs have a list of email domains where they do NOT want such emails to be looked up on + * Some orgs have a list of email domains where they do NOT want OR want such emails to be looked up on * public sources (such as Attester). This is because they already have other means to obtain - * public keys for these domains, such as from their own internal keyserver + * public keys for these domains, such as from their own internal keyserver. */ fun canLookupThisRecipientOnAttester(emailAddr: String): Boolean { - if (disallowLookupOnAttester()) { - return false - } - val userDomain = EmailUtil.getDomain(emailAddr) if (userDomain.isEmpty()) { throw IllegalStateException("Not a valid email $emailAddr") } - val disallowedDomains = disallowAttesterSearchForDomains ?: emptyList() - return !disallowedDomains.any { it.equals(userDomain, true) } - } - - /** - * - * Some orgs might want to disallow lookup on attester completely - */ - fun disallowLookupOnAttester(): Boolean { - return (disallowAttesterSearchForDomains ?: emptyList()).contains("*") + val allowedDomains = allowAttesterSearchOnlyForDomains + return if (allowedDomains != null) { + allowedDomains.any { it.equals(userDomain, true) } + } else { + val disallowedDomains = disallowAttesterSearchForDomains ?: emptyList() + if (disallowedDomains.contains("*")) { + false + } else disallowedDomains.none { it.equals(userDomain, true) } + } } /** diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/AccountPublicKeyServersViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/AccountPublicKeyServersViewModel.kt index 42d7993a09..4c927c35db 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/AccountPublicKeyServersViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/AccountPublicKeyServersViewModel.kt @@ -104,7 +104,7 @@ class AccountPublicKeyServersViewModel(application: Application) : AccountViewMo for (email in emails) { val pubResponseResult = apiRepository.pubLookup( context = getApplication(), - identData = email, + email = email, orgRules = accountEntity.clientConfiguration ) pubResponseResult.data?.pubkey?.let { key -> diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/RecipientsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/RecipientsViewModel.kt index d14df3751f..ee908a9512 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/RecipientsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/RecipientsViewModel.kt @@ -269,7 +269,7 @@ class RecipientsViewModel(application: Application) : AccountViewModel(applicati controlledRunnerForPubKeysFromServer.cancelPreviousThenRun { return@cancelPreviousThenRun apiRepository.pubLookup( context = getApplication(), - identData = email, + email = email, orgRules = activeAccount?.clientConfiguration ) } @@ -312,7 +312,7 @@ class RecipientsViewModel(application: Application) : AccountViewModel(applicati val activeAccount = getActiveAccountSuspend() val response = apiRepository.pubLookup( context = getApplication(), - identData = email, + email = email, orgRules = activeAccount?.clientConfiguration )