Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 2af8f529 authored by Romain Hunault's avatar Romain Hunault 🚴🏻
Browse files

feat(workspace): migrate legacy Murena accounts to explicit backend identity

parent 2350cf23
Loading
Loading
Loading
Loading
Loading
+24 −1
Original line number Diff line number Diff line
@@ -6,10 +6,14 @@ package at.bitfire.davdroid.syncadapter

import android.accounts.Account
import android.accounts.AccountManager
import android.net.Uri
import android.os.Bundle
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.SettingsManager
import com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_OC_BASE_URL
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert.assertEquals
@@ -37,6 +41,7 @@ class AccountUtilsTest {

    val context by lazy { InstrumentationRegistry.getInstrumentation().targetContext }
    val account by lazy { Account("Test Account", context.getString(R.string.account_type)) }
    val murenaAccount by lazy { Account("Murena Test Account", context.getString(R.string.eelo_account_type)) }

    @Test
    fun testCreateAccount() {
@@ -56,4 +61,22 @@ class AccountUtilsTest {
        }
    }

    @Test
    fun isMurenaAccount_persistsWorkspaceDomainForLegacyAccounts() {
        val userData = Bundle(1)
        userData.putString(KEY_OC_BASE_URL, BuildConfig.MURENA_BASE_URL_PRODUCTION)

        try {
            assertTrue(AccountUtils.createAccount(context, murenaAccount, userData))
            assertTrue(AccountUtils.isMurenaAccount(context, murenaAccount))

            val expectedDomain = Uri.parse(AccountUtils.extractBaseUrl(BuildConfig.MURENA_BASE_URL_PRODUCTION)).host
            val storedDomain = AccountManager.get(context).getUserData(murenaAccount, AccountSettings.KEY_WORKSPACE_DOMAIN)
            assertEquals(expectedDomain, storedDomain)
        } finally {
            val futureResult = AccountManager.get(context).removeAccount(murenaAccount, {}, null)
            assertTrue(futureResult.getResult(10, TimeUnit.SECONDS))
        }
    }

}
+1 −1
Original line number Diff line number Diff line
@@ -60,7 +60,7 @@ class AccountSettings(

    companion object {

        const val CURRENT_VERSION = 15
        const val CURRENT_VERSION = 16
        const val KEY_SETTINGS_VERSION = "version"

        const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks"
+40 −1
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@ import android.util.Base64
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
@@ -23,6 +24,7 @@ import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.davdroid.syncadapter.AccountUtils
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.util.setAndVerifyUserData
@@ -33,6 +35,7 @@ import at.bitfire.ical4android.UnknownProperty
import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.vcard4android.GroupMethod
import at.techbee.jtx.JtxContract.asSyncAdapter
import com.owncloud.android.lib.common.accounts.AccountUtils.Constants as NCAccountUtilsConstants
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.property.Url
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@@ -50,6 +53,42 @@ class AccountSettingsMigrations(
    val accountSettings: AccountSettings
) {

    /**
     * Persist explicit workspace backend identity for legacy Murena accounts that predate
     * descriptor persistence.
     */
    fun update_15_16() {
        if (account.type != context.getString(R.string.eelo_account_type)) {
            return
        }

        if (!accountManager.getUserData(account, AccountSettings.KEY_WORKSPACE_DOMAIN).isNullOrBlank()) {
            return
        }

        val baseUrl = accountManager
            .getUserData(account, NCAccountUtilsConstants.KEY_OC_BASE_URL)
            ?.let(AccountUtils::extractBaseUrl)
            ?: return

        val murenaBaseUrls = setOf(
            AccountUtils.extractBaseUrl(BuildConfig.MURENA_BASE_URL_PRODUCTION),
            AccountUtils.extractBaseUrl(BuildConfig.MURENA_BASE_URL_STAGING)
        )

        if (baseUrl !in murenaBaseUrls) {
            return
        }

        val workspaceDomain = baseUrl.toHttpUrlOrNull()?.host
        if (workspaceDomain.isNullOrBlank()) {
            Logger.log.warning("Could not derive workspace domain from legacy base URL: $baseUrl")
            return
        }

        accountManager.setAndVerifyUserData(account, AccountSettings.KEY_WORKSPACE_DOMAIN, workspaceDomain)
    }

    /**
     * We may get upstream conflict for later update because of version number.
     * from Murena's Squad
+11 −2
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.net.Uri
import android.os.Bundle
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
@@ -175,7 +176,7 @@ object AccountUtils {
        if (workspaceDomain != null) return true

        // Legacy path: derive identity from URL and account type for accounts that predate
        // workspace descriptor persistence.
        // workspace descriptor persistence, then persist explicit backend identity.
        val urlData =
            accountManager.getUserData(account, Constants.KEY_OC_BASE_URL) ?: return false

@@ -190,7 +191,15 @@ object AccountUtils {
        val isMurenaCloud = baseUrl in murenaBaseUrls
        val isMurenaAccountType = (account.type == context.getString(R.string.eelo_account_type))

        return isMurenaCloud && isMurenaAccountType
        if (!isMurenaCloud || !isMurenaAccountType) {
            return false
        }

        Uri.parse(baseUrl).host?.let { domain ->
            accountManager.setAndVerifyUserData(account, AccountSettings.KEY_WORKSPACE_DOMAIN, domain)
        }

        return true
    }

    fun isLoggedInWithMurenaSso(context: Context, account: Account): Boolean {
+47 −0
Original line number Diff line number Diff line
# Workspace Backend Compatibility (Legacy Murena Accounts)

Related work items:
- https://gitlab.e.foundation/e/os/AccountManager/-/work_items/114
- https://gitlab.e.foundation/e/os/AccountManager/-/work_items/118

## Goal

Keep already provisioned public Murena accounts working while moving to explicit backend
identity and backend-portable behavior.

## Account Shapes and Compatibility Path

1. New Murena Workspace account
- Shape: `workspace_domain` is present in Android account userData.
- Behavior: account is identified from explicit backend metadata.
- Compatibility note: no legacy fallback needed.

2. Legacy public Murena account (pre-descriptor persistence)
- Shape: `workspace_domain` is missing, but account type is `eelo_account_type` and
  `oc_base_url` matches known Murena public base URLs.
- Behavior: legacy detection remains available, then `workspace_domain` is persisted.
- Compatibility note: this is a lazy resolution path to converge to explicit metadata.

3. Migrated legacy Murena account
- Shape: account was created before descriptor persistence, then migration `15 -> 16` ran.
- Behavior: `workspace_domain` is backfilled from legacy base URL and persisted durably.
- Compatibility note: subsequent behavior no longer depends on implicit Murena defaults.

4. Non-Murena or custom backend account
- Shape: no `workspace_domain`, and Murena legacy URL/type check does not match.
- Behavior: not treated as Murena Workspace account.
- Compatibility note: no Murena-specific backfill is applied.

## Strategy Summary

- Migration path: account settings migration `update_15_16` backfills explicit backend identity
  for compatible legacy public Murena accounts.
- Lazy path: legacy detection in `isMurenaAccount()` persists `workspace_domain` when it can
  infer Murena identity safely.
- Safety: only known public Murena base URLs are used for legacy backfill.

## Expected Outcome

- Existing public Murena accounts continue to function.
- Backend identity becomes explicit and durable over time.
- Runtime behavior progressively stops depending on implicit defaults.