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
Expand Up @@ -21,6 +21,10 @@ import android.content.Context
import com.wire.android.BuildConfig
import com.wire.android.config.ServerConfigProvider
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.emm.AndroidUserContextProvider
import com.wire.android.emm.AndroidUserContextProviderImpl
import com.wire.android.emm.ManagedConfigParser
import com.wire.android.emm.ManagedConfigParserImpl
import com.wire.android.emm.ManagedConfigurationsManager
import com.wire.android.emm.ManagedConfigurationsManagerImpl
import com.wire.android.util.EMPTY
Expand All @@ -42,15 +46,33 @@ class ManagedConfigurationsModule {
@Singleton
fun provideServerConfigProvider(): ServerConfigProvider = ServerConfigProvider()

@Provides
@Singleton
fun provideAndroidUserContextProvider(): AndroidUserContextProvider =
AndroidUserContextProviderImpl()

@Provides
@Singleton
fun provideManagedConfigParser(
userContextProvider: AndroidUserContextProvider
): ManagedConfigParser = ManagedConfigParserImpl(userContextProvider)

@Provides
@Singleton
fun provideManagedConfigurationsRepository(
@ApplicationContext context: Context,
dispatcherProvider: DispatcherProvider,
serverConfigProvider: ServerConfigProvider,
globalDataStore: GlobalDataStore
globalDataStore: GlobalDataStore,
configParser: ManagedConfigParser
): ManagedConfigurationsManager {
return ManagedConfigurationsManagerImpl(context, dispatcherProvider, serverConfigProvider, globalDataStore)
return ManagedConfigurationsManagerImpl(
context,
dispatcherProvider,
serverConfigProvider,
globalDataStore,
configParser
)
}

@Provides
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Wire
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.emm

import android.os.Process

/**
* Provides the current Android user context for multi-app MDM configurations.
*
* On Android, each user (including work profiles) has a unique user ID.
* The user ID is calculated as UID / 100000, where UID is the process UID.
* - User 0: Main user (UID 0-99999)
* - User 10: Work profile or secondary user (UID 1000000-1099999)
*/
interface AndroidUserContextProvider {
/**
* Returns the current Android user ID.
* This is calculated as Process.myUid() / 100000.
*
* @return The user ID (e.g., 0 for main user, 10 for work profile)
*/
fun getCurrentAndroidUserId(): Int

/**
* Returns the current user ID as a string key for configuration lookup.
*
* @return The user ID as a string (e.g., "0", "10")
*/
fun getCurrentUserIdKey(): String

companion object {
const val DEFAULT_KEY = "default"
internal const val UID_DIVISOR = 100_000
}
}

internal class AndroidUserContextProviderImpl : AndroidUserContextProvider {

override fun getCurrentAndroidUserId(): Int =
Process.myUid() / AndroidUserContextProvider.UID_DIVISOR

override fun getCurrentUserIdKey(): String =
getCurrentAndroidUserId().toString()
}
157 changes: 157 additions & 0 deletions app/src/main/kotlin/com/wire/android/emm/ManagedConfigParser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* Wire
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.emm

import com.wire.android.appLogger
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.jsonObject

/**
* Parser for MDM managed configurations that supports both unified and context-mapped formats.
*
* **Unified Format (backward compatible):**
* ```json
* {
* "title": "Enterprise Server",
* "endpoints": { ... }
* }
* ```
*
* **Context-Mapped Format (multi-app support):**
* ```json
* {
* "0": { "title": "Secure Server", "endpoints": { ... } },
* "default": { "title": "General Server", "endpoints": { ... } }
* }
* ```
*
* The parser automatically detects the format and resolves the appropriate configuration
* based on the current Android user context.
*/
interface ManagedConfigParser {
/**
* Parses server configuration from raw JSON string.
*
* @param rawJson The raw JSON string from MDM restrictions
* @return Parsed [ManagedServerConfig] or null if parsing fails or no config found
* @throws InvalidManagedConfig if JSON is malformed
*/
fun parseServerConfig(rawJson: String): ManagedServerConfig?

/**
* Parses SSO code configuration from raw JSON string.
*
* @param rawJson The raw JSON string from MDM restrictions
* @return Parsed [ManagedSSOCodeConfig] or null if parsing fails or no config found
* @throws InvalidManagedConfig if JSON is malformed
*/
fun parseSSOCodeConfig(rawJson: String): ManagedSSOCodeConfig?
}

internal class ManagedConfigParserImpl(
private val userContextProvider: AndroidUserContextProvider
) : ManagedConfigParser {

private val json: Json = Json { ignoreUnknownKeys = true }
private val logger = appLogger.withTextTag(TAG)

override fun parseServerConfig(rawJson: String): ManagedServerConfig? {
return parseConfig(
rawJson = rawJson,
configType = "server",
isUnifiedFormat = ::isUnifiedServerFormat,
parseUnified = { json.decodeFromString<ManagedServerConfig>(rawJson) },
parseFromObject = { json.decodeFromJsonElement<ManagedServerConfig>(it) }
)
}

override fun parseSSOCodeConfig(rawJson: String): ManagedSSOCodeConfig? {
return parseConfig(
rawJson = rawJson,
configType = "SSO",
isUnifiedFormat = ::isUnifiedSSOFormat,
parseUnified = { json.decodeFromString<ManagedSSOCodeConfig>(rawJson) },
parseFromObject = { json.decodeFromJsonElement<ManagedSSOCodeConfig>(it) }
)
}

@Suppress("TooGenericExceptionCaught")
private inline fun <T> parseConfig(
rawJson: String,
configType: String,
isUnifiedFormat: (JsonObject) -> Boolean,
parseUnified: () -> T,
parseFromObject: (JsonObject) -> T
): T? {
return try {
val jsonObject = json.parseToJsonElement(rawJson).jsonObject

if (isUnifiedFormat(jsonObject)) {
logger.i("Detected unified $configType config format")
parseUnified()
} else {
logger.i("Detected context-mapped $configType config format")
resolveContextMappedConfig(jsonObject, parseFromObject)
}
} catch (e: Exception) {
throw InvalidManagedConfig("Failed to parse managed $configType config: ${e.message}")
}
}

private inline fun <T> resolveContextMappedConfig(
jsonObject: JsonObject,
parseFromObject: (JsonObject) -> T
): T? {
val userIdKey = userContextProvider.getCurrentUserIdKey()
logger.i("Resolving context-mapped config for user ID key: $userIdKey")

// Try to find config by user ID key
val configObject = jsonObject[userIdKey]?.jsonObject
?: jsonObject[AndroidUserContextProvider.DEFAULT_KEY]?.jsonObject

return if (configObject != null) {
val resolvedKey = if (jsonObject.containsKey(userIdKey)) userIdKey else AndroidUserContextProvider.DEFAULT_KEY
logger.i("Resolved config using key: $resolvedKey")
parseFromObject(configObject)
} else {
logger.w("No config found for user ID key '$userIdKey' and no '${AndroidUserContextProvider.DEFAULT_KEY}' fallback")
null
}
}

/**
* Unified server format has "endpoints" and "title" at the root level.
*/
private fun isUnifiedServerFormat(jsonObject: JsonObject): Boolean =
jsonObject.containsKey(KEY_ENDPOINTS) && jsonObject.containsKey(KEY_TITLE)

/**
* Unified SSO format has "sso_code" at the root level.
*/
private fun isUnifiedSSOFormat(jsonObject: JsonObject): Boolean =
jsonObject.containsKey(KEY_SSO_CODE)

companion object {
private const val TAG = "ManagedConfigParser"
private const val KEY_ENDPOINTS = "endpoints"
private const val KEY_TITLE = "title"
private const val KEY_SSO_CODE = "sso_code"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import java.util.concurrent.atomic.AtomicReference

interface ManagedConfigurationsManager {
Expand Down Expand Up @@ -92,9 +91,9 @@ internal class ManagedConfigurationsManagerImpl(
private val dispatchers: DispatcherProvider,
private val serverConfigProvider: ServerConfigProvider,
private val globalDataStore: GlobalDataStore,
private val configParser: ManagedConfigParser,
) : ManagedConfigurationsManager {

private val json: Json = Json { ignoreUnknownKeys = true }
private val logger = appLogger.withTextTag(TAG)
private val restrictionsManager: RestrictionsManager by lazy {
context.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
Expand Down Expand Up @@ -170,14 +169,21 @@ internal class ManagedConfigurationsManagerImpl(
return@withContext SSOCodeConfigResult.Empty
}

val rawJson = restrictions.getString(ManagedConfigurationsKeys.SSO_CODE.asKey())
if (rawJson.isNullOrBlank()) {
logger.i("No SSO code restriction found")
return@withContext SSOCodeConfigResult.Empty
}

return@withContext try {
val ssoCode = getJsonRestrictionByKey<ManagedSSOCodeConfig>(
ManagedConfigurationsKeys.SSO_CODE.asKey()
)
val ssoCode = configParser.parseSSOCodeConfig(rawJson)

if (ssoCode?.isValid == true) {
logger.i("Managed SSO code found: $ssoCode")
SSOCodeConfigResult.Success(ssoCode)
} else if (ssoCode == null) {
logger.w("No SSO code config resolved for current user context")
SSOCodeConfigResult.Empty
} else {
logger.w("Managed SSO code is not valid: $ssoCode")
SSOCodeConfigResult.Failure("Managed SSO code is not a valid config. Check the format.")
Expand All @@ -195,13 +201,20 @@ internal class ManagedConfigurationsManagerImpl(
return@withContext ServerConfigResult.Empty
}

val rawJson = restrictions.getString(ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey())
if (rawJson.isNullOrBlank()) {
logger.i("No server config restriction found")
return@withContext ServerConfigResult.Empty
}

return@withContext try {
val managedServerConfig = getJsonRestrictionByKey<ManagedServerConfig>(
ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey()
)
val managedServerConfig = configParser.parseServerConfig(rawJson)
if (managedServerConfig?.endpoints?.isValid == true) {
logger.i("Managed server config found: $managedServerConfig")
ServerConfigResult.Success(managedServerConfig)
} else if (managedServerConfig == null) {
logger.w("No server config resolved for current user context")
ServerConfigResult.Empty
} else {
logger.w("Managed server config is not valid: $managedServerConfig")
ServerConfigResult.Failure("Managed server config is not a valid config. Check the URLs and format.")
Expand All @@ -212,16 +225,6 @@ internal class ManagedConfigurationsManagerImpl(
}
}

@Suppress("TooGenericExceptionCaught")
private inline fun <reified T> getJsonRestrictionByKey(key: String): T? =
restrictionsManager.applicationRestrictions.getString(key)?.let {
try {
json.decodeFromString<T>(it)
} catch (e: Exception) {
throw InvalidManagedConfig("Failed to parse managed config for key $key: ${e.message}")
}
}

companion object {
private const val TAG = "ManagedConfigurationsManager"
}
Expand Down
Loading
Loading