Skip to content

Commit dcaa450

Browse files
authored
Merge pull request #2553 from e-foundation/workaccount-store
This adds support for work accounts, installing apps from the work app store and split packages
2 parents 4b12d45 + df9d7cb commit dcaa450

File tree

123 files changed

+4303
-641
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

123 files changed

+4303
-641
lines changed

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ buildscript {
2727

2828
ext.slf4jVersion = '1.7.36'
2929
ext.volleyVersion = '1.2.1'
30-
ext.wireVersion = '4.8.0'
30+
ext.okHttpVersion = '4.12.0'
31+
ext.ktorVersion = '2.3.12'
32+
ext.wireVersion = '4.9.9'
3133
ext.tinkVersion = '1.13.0'
3234

3335
ext.androidBuildGradleVersion = '8.2.2'
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2023 e foundation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
apply plugin: 'com.android.library'
7+
apply plugin: 'maven-publish'
8+
apply plugin: 'signing'
9+
10+
android {
11+
namespace "com.google.android.gms.auth.workaccount"
12+
13+
compileSdkVersion androidCompileSdk
14+
buildToolsVersion "$androidBuildVersionTools"
15+
16+
buildFeatures {
17+
aidl = true
18+
}
19+
20+
defaultConfig {
21+
versionName version
22+
minSdkVersion androidMinSdk
23+
targetSdkVersion androidTargetSdk
24+
}
25+
26+
compileOptions {
27+
sourceCompatibility = 1.8
28+
targetCompatibility = 1.8
29+
}
30+
31+
}
32+
33+
apply from: '../gradle/publish-android.gradle'
34+
35+
description = 'microG implementation of managed work account support'
36+
37+
dependencies {
38+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2023 e foundation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
apply plugin: 'com.android.library'
7+
apply plugin: 'kotlin-android'
8+
9+
dependencies {
10+
api project(':play-services-auth-workaccount')
11+
api project(':play-services-auth')
12+
implementation project(':play-services-base-core')
13+
14+
implementation "androidx.appcompat:appcompat:$appcompatVersion"
15+
}
16+
17+
android {
18+
namespace "com.google.android.gms.auth.workaccount"
19+
20+
compileSdkVersion androidCompileSdk
21+
buildToolsVersion "$androidBuildVersionTools"
22+
23+
defaultConfig {
24+
versionName version
25+
minSdkVersion androidMinSdk
26+
targetSdkVersion androidTargetSdk
27+
}
28+
29+
sourceSets {
30+
main.java.srcDirs += 'src/main/kotlin'
31+
}
32+
33+
compileOptions {
34+
sourceCompatibility = 1.8
35+
targetCompatibility = 1.8
36+
}
37+
38+
kotlinOptions {
39+
jvmTarget = 1.8
40+
}
41+
42+
lintOptions {
43+
disable 'MissingTranslation'
44+
}
45+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<!--
3+
~ SPDX-FileCopyrightText: 2023 e foundation
4+
~ SPDX-License-Identifier: Apache-2.0
5+
-->
6+
7+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
8+
9+
<uses-permission
10+
android:name="android.permission.AUTHENTICATE_ACCOUNTS"
11+
android:maxSdkVersion="22" />
12+
<uses-permission
13+
android:name="android.permission.GET_ACCOUNTS"
14+
android:maxSdkVersion="22" />
15+
<uses-permission
16+
android:name="android.permission.MANAGE_ACCOUNTS"
17+
android:maxSdkVersion="22" />
18+
19+
<application>
20+
21+
<service android:name="org.microg.gms.auth.workaccount.WorkAccountService"
22+
android:exported="true">
23+
<intent-filter>
24+
<action android:name="com.google.android.gms.auth.account.workaccount.START" />
25+
</intent-filter>
26+
</service>
27+
28+
<service
29+
android:name="com.google.android.gms.auth.account.authenticator.WorkAccountAuthenticatorService"
30+
android:process=":persistent"
31+
android:enabled="false"
32+
android:exported="false">
33+
34+
<intent-filter>
35+
<action android:name="android.accounts.AccountAuthenticator"/>
36+
</intent-filter>
37+
38+
<meta-data
39+
android:name="android.accounts.AccountAuthenticator"
40+
android:resource="@xml/auth_work_authenticator"/>
41+
42+
<meta-data
43+
android:name="android.accounts.AccountAuthenticator.customTokens"
44+
android:value="1"/>
45+
46+
</service>
47+
48+
</application>
49+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2024 e foundation
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package com.google.android.gms.auth.account.authenticator
7+
8+
import android.accounts.AbstractAccountAuthenticator
9+
import android.accounts.Account
10+
import android.accounts.AccountAuthenticatorResponse
11+
import android.accounts.AccountManager
12+
import android.content.Context
13+
import android.content.Intent
14+
import android.os.Build
15+
import android.os.Bundle
16+
import android.util.Log
17+
import com.google.android.gms.auth.workaccount.R
18+
import org.microg.gms.auth.AuthConstants
19+
import org.microg.gms.common.PackageUtils
20+
import org.microg.gms.auth.AuthRequest
21+
import org.microg.gms.auth.AuthResponse
22+
import org.microg.gms.auth.workaccount.WorkProfileSettings
23+
import java.io.IOException
24+
import kotlin.jvm.Throws
25+
26+
class WorkAccountAuthenticator(val context: Context) : AbstractAccountAuthenticator(context) {
27+
28+
override fun editProperties(
29+
response: AccountAuthenticatorResponse,
30+
accountType: String?
31+
): Bundle {
32+
TODO("Not yet implemented: editProperties")
33+
}
34+
35+
override fun addAccount(
36+
response: AccountAuthenticatorResponse,
37+
accountType: String,
38+
authTokenType: String?,
39+
requiredFeatures: Array<out String>?,
40+
options: Bundle
41+
): Bundle? {
42+
43+
if (!WorkProfileSettings(context).allowCreateWorkAccount) {
44+
return Bundle().apply {
45+
putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION)
46+
putString(AccountManager.KEY_ERROR_MESSAGE, context.getString(R.string.auth_work_authenticator_disabled_error)
47+
)
48+
}
49+
} else if (
50+
!options.containsKey(KEY_ACCOUNT_CREATION_TOKEN)
51+
|| options.getString(KEY_ACCOUNT_CREATION_TOKEN) == null
52+
|| options.getInt(AccountManager.KEY_CALLER_UID) != android.os.Process.myUid()) {
53+
Log.e(TAG,
54+
"refusing to add account without creation token or from external app: " +
55+
"could have been manually initiated by user (not supported) " +
56+
"or by unauthorized app (not allowed)"
57+
)
58+
59+
// TODO: The error message is not automatically displayed by the settings app as of now.
60+
// We can consider showing the error message through a popup instead.
61+
62+
return Bundle().apply {
63+
putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION)
64+
putString(AccountManager.KEY_ERROR_MESSAGE, context.getString(R.string.auth_work_authenticator_add_manual_error)
65+
)
66+
}
67+
}
68+
69+
val oauthToken: String = options.getString(KEY_ACCOUNT_CREATION_TOKEN)!!
70+
71+
try {
72+
tryAddAccount(oauthToken, response)
73+
} catch (exception: Exception) {
74+
response.onResult(Bundle().apply {
75+
putInt(
76+
AccountManager.KEY_ERROR_CODE,
77+
AccountManager.ERROR_CODE_NETWORK_ERROR
78+
)
79+
putString(AccountManager.KEY_ERROR_MESSAGE, exception.message)
80+
})
81+
}
82+
83+
/* Note: as is not documented, `null` must only be returned after `response.onResult` was
84+
* already called, hence forcing the requests to be synchronous. They are still async to
85+
* the caller's main thread because AccountManager forces potentially blocking operations,
86+
* like waiting for a response upon `addAccount`, not to be on the main thread.
87+
*/
88+
return null
89+
}
90+
91+
@Throws(Exception::class)
92+
private fun tryAddAccount(
93+
oauthToken: String,
94+
response: AccountAuthenticatorResponse
95+
) {
96+
val authResponse = AuthRequest().fromContext(context)
97+
.appIsGms()
98+
.callerIsGms()
99+
.service("ac2dm")
100+
.token(oauthToken).isAccessToken()
101+
.addAccount()
102+
.getAccountId()
103+
.droidguardResults(null)
104+
.response
105+
106+
val accountManager = AccountManager.get(context)
107+
if (accountManager.addAccountExplicitly(
108+
Account(authResponse.email, AuthConstants.WORK_ACCOUNT_TYPE),
109+
authResponse.token, Bundle().apply {
110+
// Work accounts have no SID / LSID ("BAD_COOKIE") and no first/last name.
111+
if (authResponse.accountId.isNotBlank()) {
112+
putString(KEY_GOOGLE_USER_ID, authResponse.accountId)
113+
}
114+
putString(AuthConstants.KEY_ACCOUNT_CAPABILITIES, authResponse.capabilities)
115+
putString(AuthConstants.KEY_ACCOUNT_SERVICES, authResponse.services)
116+
if (authResponse.services != "android") {
117+
Log.i(
118+
TAG,
119+
"unexpected 'services' value ${authResponse.services} (usually 'android')"
120+
)
121+
}
122+
}
123+
)
124+
) {
125+
126+
// Notify vending package
127+
context.sendBroadcast(
128+
Intent(WORK_ACCOUNT_CHANGED_BOARDCAST).setPackage("com.android.vending")
129+
)
130+
131+
// Report successful creation to caller
132+
response.onResult(Bundle().apply {
133+
putString(AccountManager.KEY_ACCOUNT_NAME, authResponse.email)
134+
putString(AccountManager.KEY_ACCOUNT_TYPE, AuthConstants.WORK_ACCOUNT_TYPE)
135+
})
136+
}
137+
}
138+
139+
override fun confirmCredentials(
140+
response: AccountAuthenticatorResponse?,
141+
account: Account?,
142+
options: Bundle?
143+
): Bundle {
144+
return Bundle().apply {
145+
putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true)
146+
}
147+
}
148+
149+
override fun getAuthToken(
150+
response: AccountAuthenticatorResponse?,
151+
account: Account,
152+
authTokenType: String?,
153+
options: Bundle?
154+
): Bundle {
155+
try {
156+
val authResponse: AuthResponse =
157+
AuthRequest().fromContext(context)
158+
.source("android")
159+
.app(
160+
context.packageName,
161+
PackageUtils.firstSignatureDigest(context, context.packageName)
162+
)
163+
.email(account.name)
164+
.token(AccountManager.get(context).getPassword(account))
165+
.service(authTokenType)
166+
.delegation(0, null)
167+
// .oauth2Foreground(oauth2Foreground)
168+
// .oauth2Prompt(oauth2Prompt)
169+
// .oauth2IncludeProfile(includeProfile)
170+
// .oauth2IncludeEmail(includeEmail)
171+
// .itCaveatTypes(itCaveatTypes)
172+
// .tokenRequestOptions(tokenRequestOptions)
173+
.systemPartition(true)
174+
.hasPermission(true)
175+
// .putDynamicFiledMap(dynamicFields)
176+
.appIsGms()
177+
.callerIsApp()
178+
.response
179+
180+
return Bundle().apply {
181+
putString(AccountManager.KEY_ACCOUNT_NAME, account.name)
182+
putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
183+
putString(AccountManager.KEY_AUTHTOKEN, authResponse.auth)
184+
}
185+
} catch (e: IOException) {
186+
return Bundle().apply {
187+
putInt(AccountManager.KEY_ERROR_CODE, AccountManager.ERROR_CODE_NETWORK_ERROR)
188+
putString(AccountManager.KEY_ERROR_MESSAGE, e.message)
189+
}
190+
}
191+
}
192+
193+
override fun getAuthTokenLabel(authTokenType: String?): String {
194+
TODO("Not yet implemented: getAuthTokenLabel")
195+
}
196+
197+
override fun updateCredentials(
198+
response: AccountAuthenticatorResponse?,
199+
account: Account?,
200+
authTokenType: String?,
201+
options: Bundle?
202+
): Bundle {
203+
TODO("Not yet implemented: updateCredentials")
204+
}
205+
206+
override fun hasFeatures(
207+
response: AccountAuthenticatorResponse?,
208+
account: Account?,
209+
features: Array<out String>
210+
): Bundle {
211+
Log.i(TAG, "Queried features: " + features.joinToString(", "))
212+
return Bundle().apply {
213+
putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false)
214+
}
215+
}
216+
217+
/**
218+
* Prevent accidental deletion, unlike GMS. The account can only be removed through client apps;
219+
* ideally, it would only be removed by the app that requested it to be created / the DPC
220+
* manager, though this is not enforced. On API 21, the account can also be removed by hand
221+
* because `removeAccountExplicitly` is not available on API 21.
222+
*/
223+
override fun getAccountRemovalAllowed(
224+
response: AccountAuthenticatorResponse?,
225+
account: Account?
226+
): Bundle {
227+
return Bundle().apply {
228+
putBoolean(AccountManager.KEY_BOOLEAN_RESULT,
229+
Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1
230+
)
231+
}
232+
}
233+
234+
companion object {
235+
const val TAG = "WorkAccAuthenticator"
236+
237+
const val WORK_ACCOUNT_CHANGED_BOARDCAST = "org.microg.vending.WORK_ACCOUNT_CHANGED"
238+
239+
const val KEY_ACCOUNT_CREATION_TOKEN = "creationToken"
240+
private const val KEY_GOOGLE_USER_ID = AuthConstants.GOOGLE_USER_ID
241+
}
242+
}

0 commit comments

Comments
 (0)