Skip to content

Commit 58cf6e4

Browse files
committed
feat: Add admin-controlled access for instance selector using Firebase Installation ID
- Add FeatureAccessManager to control feature access based on build variant/flavor - Debug builds and demo flavor: Always allow access - Production flavor: Check Firebase Remote Config for allowed device/user IDs - Use Firebase Installation ID for cross-platform device identification - Add BuildConfig interface for build variant and flavor detection - Add DeviceInfoProvider using Firebase Installations API - Add InstanceSelectorAccessControl model for Remote Config - Update InstanceConfigLoader to fetch access control configuration This allows admins to grant access to specific users by adding their Firebase Installation ID to Remote Config without app updates.
1 parent 7ba0e26 commit 58cf6e4

File tree

8 files changed

+378
-0
lines changed

8 files changed

+378
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright 2024 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.common
11+
12+
/**
13+
* Android implementation of BuildConfig
14+
*/
15+
class AndroidBuildConfig : BuildConfig {
16+
override val isDebug: Boolean
17+
get() = org.mifospay.BuildConfig.DEBUG
18+
19+
override val buildType: String
20+
get() = org.mifospay.BuildConfig.BUILD_TYPE
21+
22+
override val flavor: String
23+
get() = org.mifospay.BuildConfig.FLAVOR
24+
25+
override val versionName: String
26+
get() = org.mifospay.BuildConfig.VERSION_NAME
27+
28+
override val versionCode: Int
29+
get() = org.mifospay.BuildConfig.VERSION_CODE
30+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2024 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.common
11+
12+
import android.os.Build
13+
import dev.gitlive.firebase.Firebase
14+
import dev.gitlive.firebase.installations.installations
15+
16+
/**
17+
* Android implementation of DeviceInfoProvider using Firebase Installation ID
18+
*/
19+
class AndroidDeviceInfoProvider : DeviceInfoProvider {
20+
private val installations = Firebase.installations
21+
22+
override suspend fun getDeviceId(): String {
23+
return try {
24+
installations.id
25+
} catch (e: Exception) {
26+
"unknown"
27+
}
28+
}
29+
30+
override fun getDeviceModel(): String {
31+
return "${Build.MANUFACTURER} ${Build.MODEL}"
32+
}
33+
34+
override fun getOsVersion(): String {
35+
return "Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})"
36+
}
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2024 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.common
11+
12+
/**
13+
* Build configuration information
14+
*/
15+
interface BuildConfig {
16+
val isDebug: Boolean
17+
val buildType: String
18+
val flavor: String
19+
val versionName: String
20+
val versionCode: Int
21+
}
22+
23+
/**
24+
* Product flavors
25+
*/
26+
object ProductFlavor {
27+
const val DEMO = "demo"
28+
const val PRODUCTION = "prod"
29+
}
30+
31+
/**
32+
* Build types
33+
*/
34+
object BuildType {
35+
const val DEBUG = "debug"
36+
const val RELEASE = "release"
37+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2024 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.common
11+
12+
import dev.gitlive.firebase.Firebase
13+
import dev.gitlive.firebase.installations.installations
14+
15+
/**
16+
* Provides device-specific information for feature access control
17+
* Uses Firebase Installation ID for cross-platform device identification
18+
*/
19+
class DeviceInfoProvider {
20+
private val installations = Firebase.installations
21+
22+
/**
23+
* Returns Firebase Installation ID as unique device identifier
24+
* This can be shared with admins to grant access
25+
*/
26+
suspend fun getDeviceId(): String {
27+
return try {
28+
installations.id
29+
} catch (e: Exception) {
30+
"unknown"
31+
}
32+
}
33+
34+
/**
35+
* Returns the device model/name
36+
*/
37+
fun getDeviceModel(): String = getPlatformDeviceModel()
38+
39+
/**
40+
* Returns the OS version
41+
*/
42+
fun getOsVersion(): String = getPlatformOsVersion()
43+
}
44+
45+
/**
46+
* Platform-specific device model
47+
*/
48+
internal expect fun getPlatformDeviceModel(): String
49+
50+
/**
51+
* Platform-specific OS version
52+
*/
53+
internal expect fun getPlatformOsVersion(): String
54+
55+
/**
56+
* Data class containing device information for sharing with admins
57+
*/
58+
data class DeviceInfo(
59+
val deviceId: String,
60+
val deviceModel: String,
61+
val osVersion: String,
62+
)
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2024 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.common
11+
12+
/**
13+
* iOS/Native implementation of BuildConfig
14+
*/
15+
class NativeBuildConfig : BuildConfig {
16+
override val isDebug: Boolean
17+
get() = Platform.isDebugBinary
18+
19+
override val buildType: String
20+
get() = if (Platform.isDebugBinary) BuildType.DEBUG else BuildType.RELEASE
21+
22+
override val flavor: String
23+
get() = ProductFlavor.PRODUCTION // iOS doesn't have flavors by default
24+
25+
override val versionName: String
26+
get() = "1.0.0" // TODO: Get from iOS bundle
27+
28+
override val versionCode: Int
29+
get() = 1 // TODO: Get from iOS bundle
30+
}
31+
32+
/**
33+
* Platform utilities for native platforms
34+
*/
35+
internal expect object Platform {
36+
val isDebugBinary: Boolean
37+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2024 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.model.instance
11+
12+
import kotlinx.serialization.Serializable
13+
14+
@Serializable
15+
data class InstanceSelectorAccessControl(
16+
val enabled: Boolean = true,
17+
val allowedDeviceIds: List<String> = emptyList(),
18+
val allowedUserIds: List<String> = emptyList(),
19+
)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright 2024 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mobile-wallet/blob/master/LICENSE.md
9+
*/
10+
package org.mifospay.core.network.config
11+
12+
import org.mifospay.core.common.BuildConfig
13+
import org.mifospay.core.common.BuildType
14+
import org.mifospay.core.common.DeviceInfoProvider
15+
import org.mifospay.core.common.ProductFlavor
16+
import org.mifospay.core.datastore.UserPreferencesRepository
17+
import org.mifospay.core.model.instance.InstanceSelectorAccessControl
18+
19+
/**
20+
* Manages access control for the instance selector feature
21+
*
22+
* Access Rules:
23+
* 1. Debug build variant: Always allowed
24+
* 2. Demo product flavor: Always allowed
25+
* 3. Production flavor + Release build: Controlled by Firebase Remote Config
26+
*/
27+
class FeatureAccessManager(
28+
private val buildConfig: BuildConfig,
29+
private val deviceInfoProvider: DeviceInfoProvider,
30+
private val instanceConfigLoader: InstanceConfigLoader,
31+
private val userPreferencesRepository: UserPreferencesRepository,
32+
) {
33+
/**
34+
* Check if the instance selector feature is accessible
35+
*/
36+
suspend fun canAccessInstanceSelector(): Boolean {
37+
// Debug builds: Always allow
38+
if (buildConfig.isDebug || buildConfig.buildType == BuildType.DEBUG) {
39+
return true
40+
}
41+
42+
// Demo flavor: Always allow
43+
if (buildConfig.flavor == ProductFlavor.DEMO) {
44+
return true
45+
}
46+
47+
// Production flavor: Check Remote Config
48+
return checkRemoteConfigAccess()
49+
}
50+
51+
/**
52+
* Get the device identifier that can be shared with admin
53+
*/
54+
suspend fun getDeviceIdentifier(): String {
55+
return deviceInfoProvider.getDeviceId()
56+
}
57+
58+
/**
59+
* Get the user identifier (if logged in) that can be shared with admin
60+
*/
61+
suspend fun getUserIdentifier(): String? {
62+
val clientId = userPreferencesRepository.clientId.value
63+
return clientId?.toString()
64+
}
65+
66+
/**
67+
* Get device information for sharing with admin
68+
*/
69+
suspend fun getDeviceInfo(): DeviceInfo {
70+
return DeviceInfo(
71+
deviceId = getDeviceIdentifier(),
72+
userId = getUserIdentifier(),
73+
deviceModel = deviceInfoProvider.getDeviceModel(),
74+
osVersion = deviceInfoProvider.getOsVersion(),
75+
buildType = buildConfig.buildType,
76+
flavor = buildConfig.flavor,
77+
versionName = buildConfig.versionName,
78+
)
79+
}
80+
81+
private suspend fun checkRemoteConfigAccess(): Boolean {
82+
val accessControl = instanceConfigLoader.fetchAccessControl()
83+
84+
// If feature is globally disabled, deny access
85+
if (!accessControl.enabled) {
86+
return false
87+
}
88+
89+
val deviceId = getDeviceIdentifier()
90+
val userId = getUserIdentifier()
91+
92+
// Check if device ID is in allowed list
93+
if (accessControl.allowedDeviceIds.contains(deviceId)) {
94+
return true
95+
}
96+
97+
// Check if user ID is in allowed list
98+
if (userId != null && accessControl.allowedUserIds.contains(userId)) {
99+
return true
100+
}
101+
102+
// If no restrictions (empty lists), allow all
103+
if (accessControl.allowedDeviceIds.isEmpty() && accessControl.allowedUserIds.isEmpty()) {
104+
return true
105+
}
106+
107+
return false
108+
}
109+
}
110+
111+
data class DeviceInfo(
112+
val deviceId: String,
113+
val userId: String?,
114+
val deviceModel: String,
115+
val osVersion: String,
116+
val buildType: String,
117+
val flavor: String,
118+
val versionName: String,
119+
)

0 commit comments

Comments
 (0)