Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<MainActivity>(
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"
}
}
Original file line number Diff line number Diff line change
@@ -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<MainActivity>(
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)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<PubResponse>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class FlowcryptApiRepository : ApiRepository {
override suspend fun pubLookup(
requestCode: Long,
context: Context,
identData: String,
email: String,
orgRules: OrgRules?
): Result<PubResponse> =
withContext(Dispatchers.IO) {
Expand All @@ -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
Expand All @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ data class OrgRules constructor(
@Expose val keyManagerUrl: String? = null,
@SerializedName("disallow_attester_search_for_domains")
@Expose val disallowAttesterSearchForDomains: List<String>? = null,
@SerializedName("allow_attester_search_only_for_domains")
@Expose val allowAttesterSearchOnlyForDomains: List<String>? = null,
@SerializedName("enforce_keygen_algo")
@Expose val enforceKeygenAlgo: KeyAlgo? = null,
@SerializedName("enforce_keygen_expire_months")
Expand All @@ -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
)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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) }
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down
Loading