Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(neon_framework): Implement remote wipe #2664

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
45 changes: 44 additions & 1 deletion packages/neon_framework/lib/src/blocs/accounts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ class _AccountsBloc extends Bloc implements AccountsBloc {
for (final app in allAppImplementations) {
app.blocsCache.pruneAgainst(accounts);
}
unawaited(checkRemoteWipe(accounts));
});
}

Expand All @@ -129,6 +130,7 @@ class _AccountsBloc extends Bloc implements AccountsBloc {
final weatherStatusBlocs = AccountCache<WeatherStatusBloc>();
final maintenanceModeBlocs = AccountCache<MaintenanceModeBloc>();
final referencesBlocs = AccountCache<ReferencesBloc>();
final remoteWipeChecks = <Account>{};

@override
void dispose() {
Expand All @@ -154,7 +156,7 @@ class _AccountsBloc extends Bloc implements AccountsBloc {
@override
Future<void> removeAccount(Account account) async {
try {
await _accountRepository.logOut(account.id);
await _accountRepository.logOut(account);
} on DeleteCredentialsFailure catch (error, stackTrace) {
log.info(
'Error deleting the app password.',
Expand Down Expand Up @@ -228,4 +230,45 @@ class _AccountsBloc extends Bloc implements AccountsBloc {
account: account,
capabilities: getCapabilitiesBlocFor(account).capabilities,
);

Future<void> checkRemoteWipe(BuiltList<Account> accounts) async {
for (final account in accounts) {
// Only check each account once per app start
if (remoteWipeChecks.contains(account)) {
return;
}
remoteWipeChecks.add(account);

log.finer('Checking remote wipe status for account ${account.id}.');

try {
final wipe = await _accountRepository.getRemoteWipeStatus(account);
if (!wipe) {
return;
}

log.finer('Wiping account ${account.id}.');

await removeAccount(account);

try {
await _accountRepository.postRemoteWipeSuccess(account);
} on PostRemoteWipeSuccessFailure catch (error, stackTrace) {
log.finer(
'Failed to post remote wipe success for account ${account.id}.',
error,
stackTrace,
);
}

log.finer('Wiped account ${account.id}.');
} on GetRemoteWipeStatusFailure catch (error, stackTrace) {
log.finer(
'Failed to get remote wipe status for account ${account.id}.',
error,
stackTrace,
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ final class DeleteCredentialsFailure extends AccountFailure {
const DeleteCredentialsFailure(super.error);
}

/// {@template get_remote_wipe_status_failure}
/// Thrown when getting the device remote wipe status fails.
/// {@endtemplate}
final class GetRemoteWipeStatusFailure extends AccountFailure {
/// {@macro get_remote_wipe_status_failure}
const GetRemoteWipeStatusFailure(super.error);
}

/// {@template post_remote_wipe_success_failure}
/// Thrown when posting the device remote wipe success fails.
/// {@endtemplate}
final class PostRemoteWipeSuccessFailure extends AccountFailure {
/// {@macro post_remote_wipe_success_failure}
const PostRemoteWipeSuccessFailure(super.error);
}

/// {@template account_repository}
/// A repository that manages the account data.
/// {@endtemplate}
Expand Down Expand Up @@ -275,20 +291,15 @@ class AccountRepository {
/// Logs out the user from the server.
///
/// May throw a [DeleteCredentialsFailure].
Future<void> logOut(String accountID) async {
Future<void> logOut(Account account) async {
final value = _accounts.value;

Account? account;
final accounts = value.accounts.rebuild((b) {
account = b.remove(accountID);
b.remove(account.id);
});

if (account == null) {
return;
}

var active = value.active;
if (active == accountID) {
if (active == account.id) {
active = accounts.keys.firstOrNull;
}

Expand All @@ -300,7 +311,7 @@ class AccountRepository {
_accounts.add((active: active, accounts: accounts));

try {
await account?.client.authentication.appPassword.deleteAppPassword();
await account.client.authentication.appPassword.deleteAppPassword();
} on http.ClientException catch (error, stackTrace) {
Error.throwWithStackTrace(DeleteCredentialsFailure(error), stackTrace);
}
Expand All @@ -326,4 +337,53 @@ class AccountRepository {
_accounts.add((active: accountID, accounts: value.accounts));
await _storage.saveLastAccount(accountID);
}

/// Gets the device remote wipe status.
///
/// May throw a [GetRemoteWipeStatusFailure].
Future<bool> getRemoteWipeStatus(Account account) async {
final client = buildUnauthenticatedClient(
httpClient: _httpClient,
userAgent: _userAgent,
serverURL: account.credentials.serverURL,
);

try {
final response = await client.authentication.wipe.checkWipe(
$body: core.WipeCheckWipeRequestApplicationJson(
(b) => b..token = account.credentials.appPassword,
),
);

// This is always true, as otherwise 404 is returned, but just to be safe in the future use the returned value.
return response.body.wipe;
} on http.ClientException catch (error, stackTrace) {
if (error case DynamiteStatusCodeException() when error.statusCode == 404) {
return false;
}

Error.throwWithStackTrace(GetRemoteWipeStatusFailure(error), stackTrace);
}
}

/// Posts the remote wipe success.
///
/// May throw a [PostRemoteWipeSuccessFailure].
Future<void> postRemoteWipeSuccess(Account account) async {
final client = buildUnauthenticatedClient(
httpClient: _httpClient,
userAgent: _userAgent,
serverURL: account.credentials.serverURL,
);

try {
await client.authentication.wipe.wipeDone(
$body: core.WipeWipeDoneRequestApplicationJson(
(b) => b..token = account.credentials.appPassword,
),
);
} on http.ClientException catch (error, stackTrace) {
Error.throwWithStackTrace(PostRemoteWipeSuccessFailure(error), stackTrace);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class AuthenticationClient {
required this.appPassword,
required this.clientFlowLoginV2,
required this.users,
required this.wipe,
});

final $core.$Client core;
Expand All @@ -25,6 +26,8 @@ class AuthenticationClient {
final $core.$ClientFlowLoginV2Client clientFlowLoginV2;

final $provisioning_api.$UsersClient users;

final $core.$WipeClient wipe;
}

/// Extension for getting the [AuthenticationClient].
Expand All @@ -38,5 +41,6 @@ extension AuthenticationClientExtension on NextcloudClient {
appPassword: core.appPassword,
clientFlowLoginV2: core.clientFlowLoginV2,
users: provisioningApi.users,
wipe: core.wipe,
);
}
Loading