From 7242c32f0f332420a820595b71daac86f0b8c2e3 Mon Sep 17 00:00:00 2001 From: Frank Merkel <138444693+frankmer@users.noreply.github.com> Date: Mon, 18 Nov 2024 09:19:08 +0100 Subject: [PATCH] unregister container on delete --- lib/api/impl/privacy_idea_container_api.dart | 32 ++++++++++++++++--- .../pi_server_result_value.dart | 24 ++++++++++++++ .../extensions/token_folder_extension.dart | 6 ++-- lib/model/pi_server_response.dart | 2 +- lib/model/riverpod_states/token_state.dart | 5 +-- lib/model/tokens/day_password_token.dart | 4 +-- lib/model/tokens/hotp_token.dart | 4 +-- lib/model/tokens/push_token.dart | 2 +- lib/model/tokens/totp_token.dart | 8 ++--- .../otp_auth_processor.dart | 6 ++-- lib/utils/identifiers.dart | 2 +- lib/utils/object_validator.dart | 7 ++-- .../token_container_notifier.dart | 11 +++++-- .../delete_container_action.dart | 1 + .../delete_container_action_dialog.dart | 16 +++++++--- .../transfer_delete_dontainer_dialog.dart | 21 ++++++++---- .../default_delete_action.dart | 19 ++++++----- .../token_widgets/token_widget_base.dart | 9 ++++-- 18 files changed, 130 insertions(+), 49 deletions(-) diff --git a/lib/api/impl/privacy_idea_container_api.dart b/lib/api/impl/privacy_idea_container_api.dart index 9a7181e20..8cd99dd21 100644 --- a/lib/api/impl/privacy_idea_container_api.dart +++ b/lib/api/impl/privacy_idea_container_api.dart @@ -33,6 +33,7 @@ import '../../../../../../../../utils/ecc_utils.dart'; import '../../../../../../../../utils/privacyidea_io_client.dart'; import '../../model/api_results/pi_server_results/pi_server_result_value.dart'; import '../../model/exception_errors/localized_exception.dart'; +import '../../model/exception_errors/pi_server_result_error.dart'; import '../../model/exception_errors/response_error.dart'; import '../../model/pi_server_response.dart'; import '../../model/riverpod_states/token_state.dart'; @@ -57,7 +58,7 @@ class PiContainerApi implements TokenContainerApi { Future sync(TokenContainerFinalized container, TokenState tokenState) async { final containerTokenTemplates = tokenState.containerTokens(container.serial).toTemplates(); - final maybePiTokensTemplates = (container.policies.initialTokenTransfer) ? tokenState.maybePiTokens.toTemplates() : []; + final notLinkedTokenTemplates = (container.policies.initialTokenTransfer) ? tokenState.notLinkedTokens.toTemplates() : []; final ContainerChallenge challenge = await _getChallenge(container, container.syncUrl); @@ -67,7 +68,7 @@ class PiContainerApi implements TokenContainerApi { challenge: challenge, encKeyPair: encKeyPair, otpAuthMaps: [ - for (var template in [...containerTokenTemplates, ...maybePiTokensTemplates]) template.otpAuthMapSafeToSend + for (var template in [...containerTokenTemplates, ...notLinkedTokenTemplates]) template.otpAuthMapSafeToSend ], ); @@ -91,7 +92,7 @@ class PiContainerApi implements TokenContainerApi { // MaybePiTokens Should not be deleted final mergedTemplatesWithOtps = _handleMaybePiTokens( - maybePiTokensTemplates: maybePiTokensTemplates, + maybePiTokensTemplates: notLinkedTokenTemplates, serverTokensWithOtps: serverTokensWithOtps, container: container, ); @@ -194,8 +195,29 @@ class PiContainerApi implements TokenContainerApi { @override Future unregister(TokenContainerFinalized container) async { final unregisterUrl = container.unregisterUrl; - final ContainerChallenge challenge = await _getChallenge(container, unregisterUrl); - throw UnimplementedError(); + final ContainerChallenge challenge; + try { + challenge = await _getChallenge(container, unregisterUrl); + } on PiServerResultError catch (e) { + if (e.code == 3001) { + return true; + } + rethrow; + } + + final body = { + CONTAINER_SCOPE: unregisterUrl.toString(), + CONTAINER_CHAL_SIGNATURE: container.signMessage('${challenge.nonce}|${challenge.timeStamp}|${container.serial}|$unregisterUrl'), + }; + + final response = await _ioClient.doPost(url: unregisterUrl, body: body); + + final piResponse = response.asPiServerResponse(); + final errorResponse = piResponse?.asError; + if (errorResponse != null) throw errorResponse.piServerResultError; + if (response.statusCode != 200 || piResponse == null) throw ResponseError(response); + + return piResponse.asSuccess!.resultValue.success; } /* ////////////////////////////// diff --git a/lib/model/api_results/pi_server_results/pi_server_result_value.dart b/lib/model/api_results/pi_server_results/pi_server_result_value.dart index 97cceb0bf..2bd033f3a 100644 --- a/lib/model/api_results/pi_server_results/pi_server_result_value.dart +++ b/lib/model/api_results/pi_server_results/pi_server_result_value.dart @@ -43,6 +43,7 @@ sealed class PiServerResultValue extends PiServerResult { const (ContainerFinalizationResponse) => ContainerFinalizationResponse.fromJson(json) as T, const (ContainerSyncResult) => ContainerSyncResult.fromJson(json) as T, const (TransferQrData) => TransferQrData.fromJson(json) as T, + const (UnregisterContainerResultValue) => UnregisterContainerResultValue.fromJson(json) as T, _ => throw UnimplementedError('PiServerResultValue.fromJsonOfType<$T>'), }; } @@ -167,3 +168,26 @@ class TransferQrData extends PiServerResultValue { return TransferQrData(map['description'] as String, map['value'] as String); } } + +class UnregisterContainerResultValue extends PiServerResultValue { + static const String CONTAINER_UNREGISTER_SUCCESS = 'success'; + + final bool success; + + const UnregisterContainerResultValue({ + required this.success, + }); + + factory UnregisterContainerResultValue.fromJson(Map json) { + final map = validateMap( + map: json, + validators: { + CONTAINER_UNREGISTER_SUCCESS: const ObjectValidator(), + }, + name: 'UnregisterContainerResultValue#fromJson', + ); + return UnregisterContainerResultValue( + success: map[CONTAINER_UNREGISTER_SUCCESS] as bool, + ); + } +} diff --git a/lib/model/extensions/token_folder_extension.dart b/lib/model/extensions/token_folder_extension.dart index ae8999b28..9bc243b59 100644 --- a/lib/model/extensions/token_folder_extension.dart +++ b/lib/model/extensions/token_folder_extension.dart @@ -36,9 +36,9 @@ extension TokenListExtension on List { return nonPiTokens; } - List get maybePiTokens { - final maybePiTokens = where((token) => token.isPrivacyIdeaToken == null).toList(); - Logger.debug('${maybePiTokens.length}/$length tokens with "isPrivacyIdeaToken == null"'); + List get notLinkedTokens { + final maybePiTokens = where((token) => token.isPrivacyIdeaToken != false && token.containerSerial == null).toList(); + Logger.debug('${maybePiTokens.length}/$length unlinked tokens (not in container)'); return maybePiTokens; } diff --git a/lib/model/pi_server_response.dart b/lib/model/pi_server_response.dart index 30c5f38a1..33afd6dfb 100644 --- a/lib/model/pi_server_response.dart +++ b/lib/model/pi_server_response.dart @@ -132,6 +132,6 @@ class PiServerResponse with _$PiServerResponse { } factory PiServerResponse.fromResponse(Response response) { - return PiServerResponse.fromJson(jsonDecode(response.body)); + return PiServerResponse.fromJson(jsonDecode(response.body)); } } diff --git a/lib/model/riverpod_states/token_state.dart b/lib/model/riverpod_states/token_state.dart index 83d13e8ae..d1f600de0 100644 --- a/lib/model/riverpod_states/token_state.dart +++ b/lib/model/riverpod_states/token_state.dart @@ -44,7 +44,8 @@ class TokenState { List get pushTokensToRollOut => pushTokens.where((element) => !element.isRolledOut && element.rolloutState == PushTokenRollOutState.rolloutNotStarted).toList(); - List get maybePiTokens => tokens.maybePiTokens; + /// Tokens that are not in a container but possibly can + List get notLinkedTokens => tokens.notLinkedTokens; const TokenState({ required this.tokens, @@ -54,7 +55,7 @@ class TokenState { lastlyDeletedTokens = lastlyDeletedTokens ?? const []; List get tokensNotInContainer { - final tokensNotInContainer = tokens.maybePiTokens.where((token) => token.containerSerial != null).toList(); + final tokensNotInContainer = tokens.notLinkedTokens.where((token) => token.containerSerial != null).toList(); Logger.debug('${tokensNotInContainer.length}/${tokens.length} tokens not in container'); return tokensNotInContainer; } diff --git a/lib/model/tokens/day_password_token.dart b/lib/model/tokens/day_password_token.dart index 177431968..b84d44a9d 100644 --- a/lib/model/tokens/day_password_token.dart +++ b/lib/model/tokens/day_password_token.dart @@ -162,7 +162,7 @@ class DayPasswordToken extends OTPToken { OTP_AUTH_ISSUER: const ObjectValidatorNullable(), OTP_AUTH_SERIAL: const ObjectValidatorNullable(), OTP_AUTH_ALGORITHM: stringToAlgorithmsValidatorNullable, - OTP_AUTH_DIGITS: stringToIntValidatorNullable, + OTP_AUTH_DIGITS: intValidatorNullable, OTP_AUTH_SECRET_BASE32: base32SecretValidatorNullable, OTP_AUTH_PERIOD_SECONDS: stringSecondsToDurationValidatorNullable, OTP_AUTH_IMAGE: const ObjectValidatorNullable(), @@ -229,7 +229,7 @@ class DayPasswordToken extends OTPToken { /// | OTP_AUTH_LABEL: label, | /// | OTP_AUTH_ISSUER: issuer, | /// | CONTAINER_SERIAL: containerSerial, (optional) | - /// | CHECKED_CONTAINERS: checkedContainer, | + /// | CHECKED_CONTAINERS: checkedContainer, | /// | TOKEN_ID: id, | /// | OTP_AUTH_TYPE: type, | /// | OTP_AUTH_IMAGE: tokenImage, (optional) | diff --git a/lib/model/tokens/hotp_token.dart b/lib/model/tokens/hotp_token.dart index cfb3fe338..564ac9406 100644 --- a/lib/model/tokens/hotp_token.dart +++ b/lib/model/tokens/hotp_token.dart @@ -141,9 +141,9 @@ class HOTPToken extends OTPToken { OTP_AUTH_ISSUER: const ObjectValidatorNullable(), OTP_AUTH_SERIAL: const ObjectValidatorNullable(), OTP_AUTH_ALGORITHM: stringToAlgorithmsValidatorNullable, - OTP_AUTH_DIGITS: stringToIntValidatorNullable, + OTP_AUTH_DIGITS: intValidatorNullable, OTP_AUTH_SECRET_BASE32: base32SecretValidatorNullable, - OTP_AUTH_COUNTER: stringToIntValidatorNullable, + OTP_AUTH_COUNTER: intValidatorNullable, OTP_AUTH_IMAGE: const ObjectValidatorNullable(), OTP_AUTH_PIN: boolValidatorNullable, }, diff --git a/lib/model/tokens/push_token.dart b/lib/model/tokens/push_token.dart index 541b76fea..4c0564167 100644 --- a/lib/model/tokens/push_token.dart +++ b/lib/model/tokens/push_token.dart @@ -284,7 +284,7 @@ class PushToken extends Token { OTP_AUTH_PUSH_ROLLOUT_URL: stringToUriValidatorNullable, OTP_AUTH_IMAGE: stringToUriValidatorNullable, OTP_AUTH_PIN: boolValidator, - OTP_AUTH_VERSION: stringToIntValidatorNullable, + OTP_AUTH_VERSION: intValidatorNullable, }, name: 'PushToken', ); diff --git a/lib/model/tokens/totp_token.dart b/lib/model/tokens/totp_token.dart index cfbb63fa7..f73bee37b 100644 --- a/lib/model/tokens/totp_token.dart +++ b/lib/model/tokens/totp_token.dart @@ -141,9 +141,9 @@ class TOTPToken extends OTPToken { OTP_AUTH_ISSUER: const ObjectValidatorNullable(), OTP_AUTH_SERIAL: const ObjectValidatorNullable(), OTP_AUTH_ALGORITHM: stringToAlgorithmsValidatorNullable, - OTP_AUTH_DIGITS: stringToIntValidatorNullable, + OTP_AUTH_DIGITS: intValidatorNullable, OTP_AUTH_SECRET_BASE32: base32SecretValidatorNullable, - OTP_AUTH_PERIOD_SECONDS: stringToIntValidatorNullable, + OTP_AUTH_PERIOD_SECONDS: intValidatorNullable, OTP_AUTH_IMAGE: const ObjectValidatorNullable(), OTP_AUTH_PIN: boolValidatorNullable, }, @@ -176,9 +176,9 @@ class TOTPToken extends OTPToken { OTP_AUTH_ISSUER: const ObjectValidator(defaultValue: ''), OTP_AUTH_SERIAL: const ObjectValidatorNullable(), OTP_AUTH_ALGORITHM: stringToAlgorithmsValidator.withDefault(Algorithms.SHA1), - OTP_AUTH_DIGITS: stringToIntvalidator.withDefault(6), + OTP_AUTH_DIGITS: intValidator.withDefault(6), OTP_AUTH_SECRET_BASE32: base32Secretvalidator, - OTP_AUTH_PERIOD_SECONDS: stringToIntvalidator.withDefault(30), + OTP_AUTH_PERIOD_SECONDS: intValidator.withDefault(30), OTP_AUTH_IMAGE: const ObjectValidatorNullable(), OTP_AUTH_PIN: boolValidatorNullable, }, diff --git a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart index 2f7653df0..e993ee9d6 100644 --- a/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart +++ b/lib/processors/scheme_processors/token_import_scheme_processors/otp_auth_processor.dart @@ -180,9 +180,9 @@ Future? _parse2StepSecret(Uri uri) { map: queryParameters, validators: { OTP_AUTH_SECRET_BASE32: ObjectValidator(transformer: (v) => Encodings.base32.decode(v)), - OTP_AUTH_2STEP_SALT_LENTH: stringToIntvalidator, - OTP_AUTH_2STEP_OUTPUT_LENTH: stringToIntvalidator, - OTP_AUTH_2STEP_ITERATIONS: stringToIntvalidator, + OTP_AUTH_2STEP_SALT_LENTH: intValidator, + OTP_AUTH_2STEP_OUTPUT_LENTH: intValidator, + OTP_AUTH_2STEP_ITERATIONS: intValidator, }, name: '2StepSecret', ); diff --git a/lib/utils/identifiers.dart b/lib/utils/identifiers.dart index afb6a4a85..d56946d98 100644 --- a/lib/utils/identifiers.dart +++ b/lib/utils/identifiers.dart @@ -35,7 +35,7 @@ const OTP_AUTH_SERIAL = 'serial'; /// [String] (required) const OTP_AUTH_SECRET_BASE32 = 'secret'; -/// [String] (optional) default =' 0' +/// [String]/[int] (optional) default = '0' const OTP_AUTH_COUNTER = 'counter'; /// [String] (optional) default = '30' diff --git a/lib/utils/object_validator.dart b/lib/utils/object_validator.dart index 056af63b9..69d15f50d 100644 --- a/lib/utils/object_validator.dart +++ b/lib/utils/object_validator.dart @@ -41,8 +41,11 @@ final otpAuthCounterValidator = ObjectValidator( allowedValues: (v) => v >= 0, ); -final stringToIntValidatorNullable = stringToIntvalidator.nullable(); -final stringToIntvalidator = ObjectValidator(transformer: (v) => int.parse(v)); +final intValidatorNullable = intValidator.nullable(); +final intValidator = ObjectValidator(transformer: (v) { + if (v is int) return v; + return int.parse(v); +}); final intToStringValidator = ObjectValidator(transformer: (v) => (v as int).toString()); final intToStringValidatorNullable = intToStringValidator.nullable(); diff --git a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart index 12408ff14..eb87c5226 100644 --- a/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart +++ b/lib/utils/riverpod/riverpod_providers/generated_providers/token_container_notifier.dart @@ -273,9 +273,14 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler // DELETE CONTAINER - Future unregisterDelete(TokenContainerFinalized container) { - final unregisterd = _containerApi.unregister(container); - throw UnimplementedError(); + Future unregisterDelete(TokenContainerFinalized container) async { + if (!await _containerApi.unregister(container)) return await future; + + await _stateMutex.acquire(); + final newState = await _deleteContainerFromRepo(container); + await update((_) => newState); + _stateMutex.release(); + return newState; } Future deleteContainer(TokenContainer container) async { diff --git a/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart b/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart index 951a60fea..7eca11703 100644 --- a/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart +++ b/lib/views/container_view/container_widgets/container_actions/delete_container_action.dart @@ -61,6 +61,7 @@ class DeleteContainerAction extends ConsumerSlideableAction { Future _showDeleteDialog(BuildContext context, WidgetRef ref) => showDialog( context: context, + useRootNavigator: false, builder: (context) => DeleteContainerDialog(container), ); } diff --git a/lib/views/container_view/container_widgets/container_actions/delete_container_action_dialog.dart b/lib/views/container_view/container_widgets/container_actions/delete_container_action_dialog.dart index 0f5c65537..ff4e05cc5 100644 --- a/lib/views/container_view/container_widgets/container_actions/delete_container_action_dialog.dart +++ b/lib/views/container_view/container_widgets/container_actions/delete_container_action_dialog.dart @@ -71,13 +71,13 @@ class DeleteCorrespondingTokenDialog extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return DefaultDialog( title: Text(AppLocalizations.of(context)!.deleteContainerDialogTitle(container.serial)), - content: Text('Do you want to delete the corresponding token as well?'), // Text(AppLocalizations.of(context)!.deleteCorrespondingTokenDialogContent), + content: Text('Do you want to delete the corresponding tokens as well?'), // Text(AppLocalizations.of(context)!.deleteCorrespondingTokenDialogContent), hasCloseButton: true, actions: [ ElevatedDeleteButton( text: 'Only Container', onPressed: () async { - await ref.read(tokenContainerProvider.notifier).deleteContainer(container); + await _deleteContainer(ref); if (!context.mounted) return; Navigator.of(context).pop(); }, @@ -86,13 +86,21 @@ class DeleteCorrespondingTokenDialog extends ConsumerWidget { onPressed: () async { final containerTokens = ref.read(tokenProvider).containerTokens(container.serial); await ref.read(tokenProvider.notifier).removeTokens(containerTokens); - await ref.read(tokenContainerProvider.notifier).deleteContainer(container); + await _deleteContainer(ref); if (!context.mounted) return; Navigator.of(context).pop(); }, - text: 'Delete both', + text: 'Delete All', ), ], ); } + + Future _deleteContainer(WidgetRef ref) { + if (container is TokenContainerFinalized) { + return ref.read(tokenContainerProvider.notifier).unregisterDelete(container as TokenContainerFinalized); + } else { + return ref.read(tokenContainerProvider.notifier).deleteContainer(container); + } + } } diff --git a/lib/views/container_view/container_widgets/container_actions/transfer_dialogs/transfer_delete_dontainer_dialog.dart b/lib/views/container_view/container_widgets/container_actions/transfer_dialogs/transfer_delete_dontainer_dialog.dart index 2c7f2f104..aa0def840 100644 --- a/lib/views/container_view/container_widgets/container_actions/transfer_dialogs/transfer_delete_dontainer_dialog.dart +++ b/lib/views/container_view/container_widgets/container_actions/transfer_dialogs/transfer_delete_dontainer_dialog.dart @@ -67,12 +67,21 @@ class _TransferDeleteContainerDialogState extends ConsumerState DefaultDialog( title: Text('Transfer Container'), - content: Padding( - padding: const EdgeInsets.all(16), - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: 32, maxHeight: 32, minHeight: 32, minWidth: 32), - child: CircularProgressIndicator(), - ), + content: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded(flex: 2, child: SizedBox()), + Flexible( + child: Padding( + padding: const EdgeInsets.all(16), + child: AspectRatio( + aspectRatio: 1, + child: CircularProgressIndicator(), + ), + ), + ), + Expanded(flex: 2, child: SizedBox()), + ], ), ), true => DefaultDialog( diff --git a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart index fe97d5a4f..eb651f122 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/default_token_actions/default_delete_action.dart @@ -32,20 +32,23 @@ import '../slideable_action.dart'; class DefaultDeleteAction extends ConsumerSlideableAction { final Token token; + final bool isEnabled; - const DefaultDeleteAction({super.key, required this.token}); + const DefaultDeleteAction({super.key, required this.isEnabled, required this.token}); @override CustomSlidableAction build(context, ref) { return CustomSlidableAction( - backgroundColor: Theme.of(context).extension()!.deleteColor, + backgroundColor: isEnabled ? Theme.of(context).extension()!.deleteColor : Theme.of(context).extension()!.disabledColor, foregroundColor: Theme.of(context).extension()!.foregroundColor, - onPressed: (_) async { - if (token.isLocked && await lockAuth(localizedReason: AppLocalizations.of(context)!.deleteLockedToken) == false) { - return; - } - _showDialog(); - }, + onPressed: isEnabled + ? (_) async { + if (token.isLocked && await lockAuth(localizedReason: AppLocalizations.of(context)!.deleteLockedToken) == false) { + return; + } + _showDialog(); + } + : null, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, diff --git a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart index 063674091..63cbfd439 100644 --- a/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart +++ b/lib/views/main_view/main_view_widgets/token_widgets/token_widget_base.dart @@ -98,12 +98,17 @@ class _TokenWidgetBaseState extends ConsumerState { tokenDeleteable.then((value) { setState(() { - tokenIsDeleteable = value ?? false; + tokenIsDeleteable = value ?? true; }); }); final List actions = [ - if (tokenIsDeleteable) widget.deleteAction ?? DefaultDeleteAction(token: widget.token, key: Key('${widget.token.id}deleteAction')), + widget.deleteAction ?? + DefaultDeleteAction( + token: widget.token, + isEnabled: tokenIsDeleteable, + key: Key('${widget.token.id}deleteAction'), + ), widget.editAction ?? DefaultEditAction(token: widget.token, key: Key('${widget.token.id}editAction')), ]; if ((widget.token.pin == false)) {