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

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

feat(workspace): persist backend identity and metadata with Android account

- Add KEY_WORKSPACE_DOMAIN and KEY_WORKSPACE_OIDC_ISSUER constants to
  AccountSettings; these are the durable identity and resolved-metadata
  keys for Murena Workspace accounts.
- Extend initialUserData() to accept an optional MurenaWorkspaceDescriptor
  and write workspace_domain at account-creation time.
- Add workspaceDomain(), oidcIssuer(), workspaceDescriptor(), and
  persistWorkspaceDescriptor() to AccountSettings for callers that need
  to read or update stored metadata (e.g. after OIDC discovery).
- Pass MurenaServerConfig.getDescriptor() when creating eelo accounts in
  AccountDetailsFragment so workspace_domain is stored from the first login.
- Update AccountUtils.isMurenaAccount() to check KEY_WORKSPACE_DOMAIN first;
  fall back to URL+type matching for legacy accounts that predate this change.

Closes #114
parent 7ecc375d
Loading
Loading
Loading
Loading
+56 −1
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.util.UserIdFetcher
import at.bitfire.davdroid.util.AuthStatePrefUtils
import at.bitfire.davdroid.util.setAndVerifyUserData
import at.bitfire.davdroid.workspace.MurenaWorkspaceDescriptor
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.GroupMethod
import dagger.hilt.EntryPoint
@@ -126,12 +127,27 @@ class AccountSettings(

        const val AUTH_EXCEPTION_DETECTED = "auth_exception_detected"

        /** Workspace domain for Murena Workspace backend identity (bare domain, e.g. `"murena.io"`).
         * Stored at account creation; null for non-workspace or legacy accounts. */
        const val KEY_WORKSPACE_DOMAIN = "workspace_domain"

        /** OIDC issuer URL for Murena Workspace (e.g. `"https://accounts.murena.io/auth/realms/murena"`).
         * Populated after [at.bitfire.davdroid.workspace.MurenaOidcDiscovery.discover] succeeds;
         * null until discovery runs or if OIDC is not configured on the server. */
        const val KEY_WORKSPACE_OIDC_ISSUER = "workspace_oidc_issuer"

        /** Static property to indicate whether AccountSettings migration is currently running.
         * **Access must be `synchronized` with `AccountSettings::class.java`.** */
        @Volatile
        var currentlyUpdating = false

        fun initialUserData(credentials: Credentials?, url: String? = null, cookies: String? = null, email: String? = null): Bundle {
        fun initialUserData(
            credentials: Credentials?,
            url: String? = null,
            cookies: String? = null,
            email: String? = null,
            descriptor: MurenaWorkspaceDescriptor? = null
        ): Bundle {
            val bundle = Bundle()
            bundle.putString(KEY_SETTINGS_VERSION, CURRENT_VERSION.toString())

@@ -169,6 +185,11 @@ class AccountSettings(

            bundle.putString(KEY_EVENT_COLORS, ENABLED_EVENT_COLORS)

            descriptor?.let {
                bundle.putString(KEY_WORKSPACE_DOMAIN, it.workspaceDomain)
                // oidcIssuer is null at creation time; stored later via persistWorkspaceDescriptor
            }

            addUserIdToBundle(
                bundle = bundle,
                url = url,
@@ -299,6 +320,40 @@ class AccountSettings(
    }


    // workspace backend identity

    /** Returns the workspace domain stored with this account, or null for non-workspace or legacy accounts. */
    fun workspaceDomain(): String? =
        accountManager.getUserData(account, KEY_WORKSPACE_DOMAIN)

    /** Returns the OIDC issuer stored with this account, or null if discovery has not run yet. */
    fun oidcIssuer(): String? =
        accountManager.getUserData(account, KEY_WORKSPACE_OIDC_ISSUER)

    /**
     * Reconstructs a [MurenaWorkspaceDescriptor] from persisted account metadata.
     * Returns null if no workspace domain is stored (non-workspace or legacy account).
     */
    fun workspaceDescriptor(): MurenaWorkspaceDescriptor? {
        val domain = workspaceDomain() ?: return null
        return MurenaWorkspaceDescriptor(
            workspaceDomain = domain,
            oidcIssuer = oidcIssuer()
        )
    }

    /**
     * Persists the backend identity and resolved metadata from [descriptor] into this account's
     * user data. Call at account creation (to store [MurenaWorkspaceDescriptor.workspaceDomain])
     * and again after [at.bitfire.davdroid.workspace.MurenaOidcDiscovery.discover] succeeds
     * (to store [MurenaWorkspaceDescriptor.oidcIssuer]).
     */
    fun persistWorkspaceDescriptor(descriptor: MurenaWorkspaceDescriptor) {
        accountManager.setAndVerifyUserData(account, KEY_WORKSPACE_DOMAIN, descriptor.workspaceDomain)
        accountManager.setAndVerifyUserData(account, KEY_WORKSPACE_OIDC_ISSUER, descriptor.oidcIssuer)
    }


    // sync. settings

    /**
+7 −0
Original line number Diff line number Diff line
@@ -169,6 +169,13 @@ object AccountUtils {
    fun isMurenaAccount(context: Context, account: Account): Boolean {
        val accountManager = AccountManager.get(context)

        // Explicit backend identity: workspace domain stored for accounts created after descriptor
        // persistence was introduced. Its presence is sufficient to identify a Murena Workspace account.
        val workspaceDomain = accountManager.getUserData(account, AccountSettings.KEY_WORKSPACE_DOMAIN)
        if (workspaceDomain != null) return true

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

+6 −1
Original line number Diff line number Diff line
@@ -52,6 +52,7 @@ import at.bitfire.davdroid.syncadapter.AccountUtils
import at.bitfire.davdroid.syncadapter.SyncAllAccountWorker
import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.davdroid.util.AuthStatePrefUtils
import at.bitfire.davdroid.util.MurenaServerConfig
import at.bitfire.vcard4android.GroupMethod
import com.google.android.material.snackbar.Snackbar
import com.nextcloud.android.utils.AccountManagerUtils
@@ -352,7 +353,11 @@ class AccountDetailsFragment : Fragment() {
                // During Murena SSO migration, `basicAuthMurenaAccount` will be non-null. Otherwise, always null.
                val account = basicAuthMurenaAccount ?: getOrCreateAccount(name, accountType)

                val userData = AccountSettings.initialUserData(credentials, baseURL, config.cookies, config.calDAV?.emails?.firstOrNull())
                val workspaceDescriptor = if (accountType == context.getString(R.string.eelo_account_type)) {
                    MurenaServerConfig.getDescriptor(context)
                } else null

                val userData = AccountSettings.initialUserData(credentials, baseURL, config.cookies, config.calDAV?.emails?.firstOrNull(), workspaceDescriptor)

                AuthStatePrefUtils.saveAuthState(context, account, credentials?.authState?.jsonSerializeString())