Loading app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.javadeleted 100644 → 0 +0 −174 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * 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 <https://www.gnu.org/licenses/>. */ package com.owncloud.android.ui.activity; import static com.nextcloud.android.sso.Constants.DELIMITER; import static com.nextcloud.android.sso.Constants.EXCEPTION_ACCOUNT_ACCESS_DECLINED; import static com.nextcloud.android.sso.Constants.EXCEPTION_ACCOUNT_NOT_FOUND; import static com.nextcloud.android.sso.Constants.NEXTCLOUD_FILES_ACCOUNT; import static com.nextcloud.android.sso.Constants.NEXTCLOUD_SSO; import static com.nextcloud.android.sso.Constants.NEXTCLOUD_SSO_EXCEPTION; import static com.nextcloud.android.sso.Constants.SSO_SERVER_URL; import static com.nextcloud.android.sso.Constants.SSO_SHARED_PREFERENCE; import static com.nextcloud.android.sso.Constants.SSO_TOKEN; import static com.nextcloud.android.sso.Constants.SSO_USER_ID; import android.accounts.Account; import android.accounts.AccountManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.nextcloud.android.utils.EncryptionUtils; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.accounts.AccountUtils; import java.util.Arrays; import java.util.UUID; import java.util.logging.Level; import at.bitfire.davdroid.R; import at.bitfire.davdroid.log.Logger; import at.bitfire.davdroid.util.SsoUtils; public class SsoGrantPermissionActivity extends AppCompatActivity { private static final String[] ACCEPTED_PACKAGE_LIST = {"foundation.e.notes"}; private Account account; private String packageName; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sso_grant_permission); ComponentName callingActivity = getCallingActivity(); if (callingActivity != null) { packageName = callingActivity.getPackageName(); account = getIntent().getParcelableExtra(NEXTCLOUD_FILES_ACCOUNT); validateAndAutoGrandPermission(); } else { Logger.INSTANCE.getLog().log(Level.SEVERE, "SsoGrantPermissionActivity: Calling Package is null"); setResultAndExit(EXCEPTION_ACCOUNT_ACCESS_DECLINED); } } private void validateAndAutoGrandPermission() { if (!isValidRequest()) { Logger.INSTANCE.getLog().log(Level.SEVERE, "SsoGrantPermissionActivity: Invalid request"); setResultAndExit(EXCEPTION_ACCOUNT_ACCESS_DECLINED); return; } grantPermission(); } private boolean isValidRequest() { if (packageName == null || account == null) { return false; } boolean validPackage = Arrays.asList(ACCEPTED_PACKAGE_LIST) .contains(packageName); if (!validPackage) { return false; } return Arrays.asList(getAcceptedAccountTypeList()) .contains(account.type); } private String[] getAcceptedAccountTypeList() { return new String[]{ getString(R.string.eelo_account_type) }; } private void grantPermission() { String serverUrl = getServerUrl(); if (serverUrl == null) { return; } // create token String token = UUID.randomUUID().toString().replaceAll("-", ""); String userId = getUserId(); saveToken(token, account.name); setResultData(token, userId, serverUrl); finish(); } @NonNull private String getUserId() { final AccountManager accountManager = AccountManager.get(this); final String baseUrl = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL); return SsoUtils.INSTANCE.sanitizeUserId(account.name, baseUrl); } private void setResultData(String token, String userId, String serverUrl) { final Bundle result = new Bundle(); result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); result.putString(AccountManager.KEY_AUTHTOKEN, NEXTCLOUD_SSO); result.putString(SSO_USER_ID, userId); result.putString(SSO_TOKEN, token); result.putString(SSO_SERVER_URL, serverUrl); Intent data = new Intent(); data.putExtra(NEXTCLOUD_SSO, result); setResult(RESULT_OK, data); } @Nullable private String getServerUrl() { try { OwnCloudAccount ocAccount = new OwnCloudAccount(account, this); return ocAccount.getBaseUri().toString(); } catch (AccountUtils.AccountNotFoundException e) { Logger.INSTANCE.getLog().log(Level.SEVERE, "SsoGrantPermissionActivity: Account not found"); setResultAndExit(EXCEPTION_ACCOUNT_NOT_FOUND); } return null; } private void saveToken(String token, String accountName) { String hashedTokenWithSalt = EncryptionUtils.generateSHA512(token); SharedPreferences sharedPreferences = getSharedPreferences(SSO_SHARED_PREFERENCE, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(packageName + DELIMITER + accountName, hashedTokenWithSalt); editor.apply(); } private void setResultAndExit(String exception) { Intent data = new Intent(); data.putExtra(NEXTCLOUD_SSO_EXCEPTION, exception); setResult(RESULT_CANCELED, data); finish(); } } No newline at end of file app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.kt 0 → 100644 +86 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2024 * 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 <https://www.gnu.org/licenses/>. */ package com.owncloud.android.ui.activity import android.accounts.Account import android.content.Intent import android.os.Build import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import at.bitfire.davdroid.R import com.nextcloud.android.sso.Constants import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @AndroidEntryPoint class SsoGrantPermissionActivity : AppCompatActivity() { private val viewModel: SsoGrantPermissionViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_sso_grant_permission) lifecycleScope.launch { viewModel.permissionEvent .flowWithLifecycle( lifecycle = lifecycle, minActiveState = Lifecycle.State.CREATED ).collectLatest { when (it) { is SsoGrantPermissionEvent.PermissionGranted -> setSuccessResult(it.bundle) is SsoGrantPermissionEvent.PermissionDenied -> setCanceledResult(it.errorMessage) } } } validateAccount() } private fun validateAccount() { val account: Account? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(Constants.NEXTCLOUD_FILES_ACCOUNT, Account::class.java) } else { intent.getParcelableExtra(Constants.NEXTCLOUD_FILES_ACCOUNT) } viewModel.initValidation( callingActivity = callingActivity, account = account ) } private fun setCanceledResult(exception: String) { val data = Intent() data.putExtra(Constants.NEXTCLOUD_SSO_EXCEPTION, exception) setResult(RESULT_CANCELED, data) finish() } private fun setSuccessResult(result: Bundle) { val data = Intent() data.putExtra(Constants.NEXTCLOUD_SSO, result) setResult(RESULT_OK, data) finish() } } app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionEvent.kt 0 → 100644 +26 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2024 * 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 <https://www.gnu.org/licenses/>. */ package com.owncloud.android.ui.activity import android.os.Bundle sealed class SsoGrantPermissionEvent { data class PermissionGranted(val bundle: Bundle) : SsoGrantPermissionEvent() data class PermissionDenied(val errorMessage: String) : SsoGrantPermissionEvent() } app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionViewModel.kt 0 → 100644 +172 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2024 * 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 <https://www.gnu.org/licenses/>. */ package com.owncloud.android.ui.activity import android.accounts.Account import android.accounts.AccountManager import android.content.ComponentName import android.content.Context import android.os.Bundle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.log.Logger.log import at.bitfire.davdroid.util.UserIdFetcher import com.nextcloud.android.sso.Constants import com.nextcloud.android.utils.EncryptionUtils import com.owncloud.android.lib.common.OwnCloudAccount import com.owncloud.android.lib.common.accounts.AccountUtils import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import java.util.UUID import java.util.logging.Level import javax.inject.Inject @HiltViewModel class SsoGrantPermissionViewModel @Inject constructor( @ApplicationContext private val context: Context, private val database: AppDatabase, ) : ViewModel() { private val acceptedAccountTypes = listOf(context.getString(R.string.eelo_account_type)) private val acceptedPackages = listOf("foundation.e.notes") private val _permissionEvent = MutableSharedFlow<SsoGrantPermissionEvent>() val permissionEvent = _permissionEvent.asSharedFlow() fun initValidation(callingActivity: ComponentName?, account: Account?) { viewModelScope.launch(Dispatchers.IO) { val packageName = getCallingPackageName(callingActivity) ?: return@launch validate(packageName, account) } } private suspend fun emitPermissionDeniedEvent(message: String) { _permissionEvent.emit( SsoGrantPermissionEvent.PermissionDenied( errorMessage = message ) ) } private suspend fun getCallingPackageName(callingActivity: ComponentName?): String? { if (callingActivity != null) { return callingActivity.packageName } log.log(Level.SEVERE, "SsoGrantPermissionViewModel: Calling Package is null") emitPermissionDeniedEvent(Constants.EXCEPTION_ACCOUNT_ACCESS_DECLINED) return null } private suspend fun validate(packageName: String?, account: Account?) { if (!isValidRequest(packageName, account)) { log.log(Level.SEVERE, "SsoGrantPermissionViewModel: Invalid request") emitPermissionDeniedEvent(Constants.EXCEPTION_ACCOUNT_ACCESS_DECLINED) } val serverUrl = getServerUrl(account!!) ?: return val token = UUID.randomUUID().toString().replace("-".toRegex(), "") val userId = getUserId(account) saveToken( token = token, accountName = userId, packageName = packageName!! ) passSuccessfulData( account = account, token = token, userId = userId, serverUrl = serverUrl ) } private fun isValidRequest(packageName: String?, account: Account?): Boolean { if (packageName == null || account == null) { return false } return acceptedPackages.contains(packageName) && acceptedAccountTypes.contains(account.type) } private suspend fun getServerUrl(account: Account): String? { try { val ocAccount = OwnCloudAccount(account, context) return ocAccount.baseUri.toString() } catch (e: AccountUtils.AccountNotFoundException) { log.log(Level.SEVERE, "SsoGrantPermissionViewModel: Account not found") emitPermissionDeniedEvent(Constants.EXCEPTION_ACCOUNT_NOT_FOUND) } return null } private fun getUserId(account: Account): String { val accountManager = AccountManager.get(context) val userId = accountManager.getUserData(account, AccountUtils.Constants.KEY_USER_ID) if (!userId.isNullOrBlank()) { return userId } val principalUrl = database.serviceDao().getByAccountName(account.name)?.principal?.toString() ?: return account.name return UserIdFetcher.fetch(principalUrl) ?: account.name } private fun saveToken(token: String, accountName: String, packageName: String) { val hashedTokenWithSalt = EncryptionUtils.generateSHA512(token) val sharedPreferences = context.getSharedPreferences(Constants.SSO_SHARED_PREFERENCE, Context.MODE_PRIVATE) val editor = sharedPreferences.edit() editor.putString(packageName + Constants.DELIMITER + accountName, hashedTokenWithSalt) editor.apply() } private suspend fun passSuccessfulData( account: Account, token: String, userId: String, serverUrl: String ) { val result = Bundle().apply { putString(AccountManager.KEY_ACCOUNT_NAME, account.name) putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) putString(AccountManager.KEY_AUTHTOKEN, Constants.NEXTCLOUD_SSO) putString(Constants.SSO_USER_ID, userId) putString(Constants.SSO_TOKEN, token) putString(Constants.SSO_SERVER_URL, serverUrl) } _permissionEvent.emit( SsoGrantPermissionEvent.PermissionGranted( bundle = result ) ) } } app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +25 −8 Original line number Diff line number Diff line Loading @@ -19,8 +19,8 @@ import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker import at.bitfire.davdroid.syncadapter.SyncUtils import at.bitfire.davdroid.util.UserIdFetcher import at.bitfire.davdroid.util.AuthStatePrefUtils import at.bitfire.davdroid.util.SsoUtils import at.bitfire.davdroid.util.setAndVerifyUserData import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod Loading Loading @@ -158,21 +158,38 @@ class AccountSettings( bundle.putString(COOKIE_KEY, cookies) } var baseUrl : String? = null if (!url.isNullOrEmpty()) { baseUrl = AccountUtils.getOwnCloudBaseUrl(url) val baseUrl = AccountUtils.getOwnCloudBaseUrl(url) bundle.putString(NCAccountUtils.Constants.KEY_OC_BASE_URL, baseUrl) } addUserIdToBundle(bundle, userName, baseUrl) addEmailToBundle(bundle, email, userName) addUserIdToBundle( bundle = bundle, url = url, userName = userName ) addEmailToBundle( bundle = bundle, email = email, userName = userName ) return bundle } private fun addUserIdToBundle(bundle: Bundle, userName: String?, baseUrl: String?) { userName?.let { val userId = SsoUtils.sanitizeUserId(it, baseUrl) private fun addUserIdToBundle(bundle: Bundle, url: String?, userName: String?) { var userId: String? = null if (!url.isNullOrBlank()) { userId = UserIdFetcher.fetch(url) } if (userId.isNullOrBlank()) { userId = userName } userId?.let { bundle.putString(NCAccountUtils.Constants.KEY_USER_ID, userId) } } Loading Loading
app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.javadeleted 100644 → 0 +0 −174 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2023 * 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 <https://www.gnu.org/licenses/>. */ package com.owncloud.android.ui.activity; import static com.nextcloud.android.sso.Constants.DELIMITER; import static com.nextcloud.android.sso.Constants.EXCEPTION_ACCOUNT_ACCESS_DECLINED; import static com.nextcloud.android.sso.Constants.EXCEPTION_ACCOUNT_NOT_FOUND; import static com.nextcloud.android.sso.Constants.NEXTCLOUD_FILES_ACCOUNT; import static com.nextcloud.android.sso.Constants.NEXTCLOUD_SSO; import static com.nextcloud.android.sso.Constants.NEXTCLOUD_SSO_EXCEPTION; import static com.nextcloud.android.sso.Constants.SSO_SERVER_URL; import static com.nextcloud.android.sso.Constants.SSO_SHARED_PREFERENCE; import static com.nextcloud.android.sso.Constants.SSO_TOKEN; import static com.nextcloud.android.sso.Constants.SSO_USER_ID; import android.accounts.Account; import android.accounts.AccountManager; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import com.nextcloud.android.utils.EncryptionUtils; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.accounts.AccountUtils; import java.util.Arrays; import java.util.UUID; import java.util.logging.Level; import at.bitfire.davdroid.R; import at.bitfire.davdroid.log.Logger; import at.bitfire.davdroid.util.SsoUtils; public class SsoGrantPermissionActivity extends AppCompatActivity { private static final String[] ACCEPTED_PACKAGE_LIST = {"foundation.e.notes"}; private Account account; private String packageName; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sso_grant_permission); ComponentName callingActivity = getCallingActivity(); if (callingActivity != null) { packageName = callingActivity.getPackageName(); account = getIntent().getParcelableExtra(NEXTCLOUD_FILES_ACCOUNT); validateAndAutoGrandPermission(); } else { Logger.INSTANCE.getLog().log(Level.SEVERE, "SsoGrantPermissionActivity: Calling Package is null"); setResultAndExit(EXCEPTION_ACCOUNT_ACCESS_DECLINED); } } private void validateAndAutoGrandPermission() { if (!isValidRequest()) { Logger.INSTANCE.getLog().log(Level.SEVERE, "SsoGrantPermissionActivity: Invalid request"); setResultAndExit(EXCEPTION_ACCOUNT_ACCESS_DECLINED); return; } grantPermission(); } private boolean isValidRequest() { if (packageName == null || account == null) { return false; } boolean validPackage = Arrays.asList(ACCEPTED_PACKAGE_LIST) .contains(packageName); if (!validPackage) { return false; } return Arrays.asList(getAcceptedAccountTypeList()) .contains(account.type); } private String[] getAcceptedAccountTypeList() { return new String[]{ getString(R.string.eelo_account_type) }; } private void grantPermission() { String serverUrl = getServerUrl(); if (serverUrl == null) { return; } // create token String token = UUID.randomUUID().toString().replaceAll("-", ""); String userId = getUserId(); saveToken(token, account.name); setResultData(token, userId, serverUrl); finish(); } @NonNull private String getUserId() { final AccountManager accountManager = AccountManager.get(this); final String baseUrl = accountManager.getUserData(account, AccountUtils.Constants.KEY_OC_BASE_URL); return SsoUtils.INSTANCE.sanitizeUserId(account.name, baseUrl); } private void setResultData(String token, String userId, String serverUrl) { final Bundle result = new Bundle(); result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name); result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type); result.putString(AccountManager.KEY_AUTHTOKEN, NEXTCLOUD_SSO); result.putString(SSO_USER_ID, userId); result.putString(SSO_TOKEN, token); result.putString(SSO_SERVER_URL, serverUrl); Intent data = new Intent(); data.putExtra(NEXTCLOUD_SSO, result); setResult(RESULT_OK, data); } @Nullable private String getServerUrl() { try { OwnCloudAccount ocAccount = new OwnCloudAccount(account, this); return ocAccount.getBaseUri().toString(); } catch (AccountUtils.AccountNotFoundException e) { Logger.INSTANCE.getLog().log(Level.SEVERE, "SsoGrantPermissionActivity: Account not found"); setResultAndExit(EXCEPTION_ACCOUNT_NOT_FOUND); } return null; } private void saveToken(String token, String accountName) { String hashedTokenWithSalt = EncryptionUtils.generateSHA512(token); SharedPreferences sharedPreferences = getSharedPreferences(SSO_SHARED_PREFERENCE, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(packageName + DELIMITER + accountName, hashedTokenWithSalt); editor.apply(); } private void setResultAndExit(String exception) { Intent data = new Intent(); data.putExtra(NEXTCLOUD_SSO_EXCEPTION, exception); setResult(RESULT_CANCELED, data); finish(); } } No newline at end of file
app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionActivity.kt 0 → 100644 +86 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2024 * 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 <https://www.gnu.org/licenses/>. */ package com.owncloud.android.ui.activity import android.accounts.Account import android.content.Intent import android.os.Build import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import at.bitfire.davdroid.R import com.nextcloud.android.sso.Constants import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @AndroidEntryPoint class SsoGrantPermissionActivity : AppCompatActivity() { private val viewModel: SsoGrantPermissionViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_sso_grant_permission) lifecycleScope.launch { viewModel.permissionEvent .flowWithLifecycle( lifecycle = lifecycle, minActiveState = Lifecycle.State.CREATED ).collectLatest { when (it) { is SsoGrantPermissionEvent.PermissionGranted -> setSuccessResult(it.bundle) is SsoGrantPermissionEvent.PermissionDenied -> setCanceledResult(it.errorMessage) } } } validateAccount() } private fun validateAccount() { val account: Account? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra(Constants.NEXTCLOUD_FILES_ACCOUNT, Account::class.java) } else { intent.getParcelableExtra(Constants.NEXTCLOUD_FILES_ACCOUNT) } viewModel.initValidation( callingActivity = callingActivity, account = account ) } private fun setCanceledResult(exception: String) { val data = Intent() data.putExtra(Constants.NEXTCLOUD_SSO_EXCEPTION, exception) setResult(RESULT_CANCELED, data) finish() } private fun setSuccessResult(result: Bundle) { val data = Intent() data.putExtra(Constants.NEXTCLOUD_SSO, result) setResult(RESULT_OK, data) finish() } }
app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionEvent.kt 0 → 100644 +26 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2024 * 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 <https://www.gnu.org/licenses/>. */ package com.owncloud.android.ui.activity import android.os.Bundle sealed class SsoGrantPermissionEvent { data class PermissionGranted(val bundle: Bundle) : SsoGrantPermissionEvent() data class PermissionDenied(val errorMessage: String) : SsoGrantPermissionEvent() }
app/src/main/java/com/owncloud/android/ui/activity/SsoGrantPermissionViewModel.kt 0 → 100644 +172 −0 Original line number Diff line number Diff line /* * Copyright MURENA SAS 2024 * 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 <https://www.gnu.org/licenses/>. */ package com.owncloud.android.ui.activity import android.accounts.Account import android.accounts.AccountManager import android.content.ComponentName import android.content.Context import android.os.Bundle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.log.Logger.log import at.bitfire.davdroid.util.UserIdFetcher import com.nextcloud.android.sso.Constants import com.nextcloud.android.utils.EncryptionUtils import com.owncloud.android.lib.common.OwnCloudAccount import com.owncloud.android.lib.common.accounts.AccountUtils import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.launch import java.util.UUID import java.util.logging.Level import javax.inject.Inject @HiltViewModel class SsoGrantPermissionViewModel @Inject constructor( @ApplicationContext private val context: Context, private val database: AppDatabase, ) : ViewModel() { private val acceptedAccountTypes = listOf(context.getString(R.string.eelo_account_type)) private val acceptedPackages = listOf("foundation.e.notes") private val _permissionEvent = MutableSharedFlow<SsoGrantPermissionEvent>() val permissionEvent = _permissionEvent.asSharedFlow() fun initValidation(callingActivity: ComponentName?, account: Account?) { viewModelScope.launch(Dispatchers.IO) { val packageName = getCallingPackageName(callingActivity) ?: return@launch validate(packageName, account) } } private suspend fun emitPermissionDeniedEvent(message: String) { _permissionEvent.emit( SsoGrantPermissionEvent.PermissionDenied( errorMessage = message ) ) } private suspend fun getCallingPackageName(callingActivity: ComponentName?): String? { if (callingActivity != null) { return callingActivity.packageName } log.log(Level.SEVERE, "SsoGrantPermissionViewModel: Calling Package is null") emitPermissionDeniedEvent(Constants.EXCEPTION_ACCOUNT_ACCESS_DECLINED) return null } private suspend fun validate(packageName: String?, account: Account?) { if (!isValidRequest(packageName, account)) { log.log(Level.SEVERE, "SsoGrantPermissionViewModel: Invalid request") emitPermissionDeniedEvent(Constants.EXCEPTION_ACCOUNT_ACCESS_DECLINED) } val serverUrl = getServerUrl(account!!) ?: return val token = UUID.randomUUID().toString().replace("-".toRegex(), "") val userId = getUserId(account) saveToken( token = token, accountName = userId, packageName = packageName!! ) passSuccessfulData( account = account, token = token, userId = userId, serverUrl = serverUrl ) } private fun isValidRequest(packageName: String?, account: Account?): Boolean { if (packageName == null || account == null) { return false } return acceptedPackages.contains(packageName) && acceptedAccountTypes.contains(account.type) } private suspend fun getServerUrl(account: Account): String? { try { val ocAccount = OwnCloudAccount(account, context) return ocAccount.baseUri.toString() } catch (e: AccountUtils.AccountNotFoundException) { log.log(Level.SEVERE, "SsoGrantPermissionViewModel: Account not found") emitPermissionDeniedEvent(Constants.EXCEPTION_ACCOUNT_NOT_FOUND) } return null } private fun getUserId(account: Account): String { val accountManager = AccountManager.get(context) val userId = accountManager.getUserData(account, AccountUtils.Constants.KEY_USER_ID) if (!userId.isNullOrBlank()) { return userId } val principalUrl = database.serviceDao().getByAccountName(account.name)?.principal?.toString() ?: return account.name return UserIdFetcher.fetch(principalUrl) ?: account.name } private fun saveToken(token: String, accountName: String, packageName: String) { val hashedTokenWithSalt = EncryptionUtils.generateSHA512(token) val sharedPreferences = context.getSharedPreferences(Constants.SSO_SHARED_PREFERENCE, Context.MODE_PRIVATE) val editor = sharedPreferences.edit() editor.putString(packageName + Constants.DELIMITER + accountName, hashedTokenWithSalt) editor.apply() } private suspend fun passSuccessfulData( account: Account, token: String, userId: String, serverUrl: String ) { val result = Bundle().apply { putString(AccountManager.KEY_ACCOUNT_NAME, account.name) putString(AccountManager.KEY_ACCOUNT_TYPE, account.type) putString(AccountManager.KEY_AUTHTOKEN, Constants.NEXTCLOUD_SSO) putString(Constants.SSO_USER_ID, userId) putString(Constants.SSO_TOKEN, token) putString(Constants.SSO_SERVER_URL, serverUrl) } _permissionEvent.emit( SsoGrantPermissionEvent.PermissionGranted( bundle = result ) ) } }
app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +25 −8 Original line number Diff line number Diff line Loading @@ -19,8 +19,8 @@ import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.syncadapter.AccountUtils import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker import at.bitfire.davdroid.syncadapter.SyncUtils import at.bitfire.davdroid.util.UserIdFetcher import at.bitfire.davdroid.util.AuthStatePrefUtils import at.bitfire.davdroid.util.SsoUtils import at.bitfire.davdroid.util.setAndVerifyUserData import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod Loading Loading @@ -158,21 +158,38 @@ class AccountSettings( bundle.putString(COOKIE_KEY, cookies) } var baseUrl : String? = null if (!url.isNullOrEmpty()) { baseUrl = AccountUtils.getOwnCloudBaseUrl(url) val baseUrl = AccountUtils.getOwnCloudBaseUrl(url) bundle.putString(NCAccountUtils.Constants.KEY_OC_BASE_URL, baseUrl) } addUserIdToBundle(bundle, userName, baseUrl) addEmailToBundle(bundle, email, userName) addUserIdToBundle( bundle = bundle, url = url, userName = userName ) addEmailToBundle( bundle = bundle, email = email, userName = userName ) return bundle } private fun addUserIdToBundle(bundle: Bundle, userName: String?, baseUrl: String?) { userName?.let { val userId = SsoUtils.sanitizeUserId(it, baseUrl) private fun addUserIdToBundle(bundle: Bundle, url: String?, userName: String?) { var userId: String? = null if (!url.isNullOrBlank()) { userId = UserIdFetcher.fetch(url) } if (userId.isNullOrBlank()) { userId = userName } userId?.let { bundle.putString(NCAccountUtils.Constants.KEY_USER_ID, userId) } } Loading