Skip to content

Commit

Permalink
unregister container on delete
Browse files Browse the repository at this point in the history
  • Loading branch information
frankmer committed Nov 18, 2024
1 parent fc5c3e6 commit 7242c32
Show file tree
Hide file tree
Showing 18 changed files with 130 additions and 49 deletions.
32 changes: 27 additions & 5 deletions lib/api/impl/privacy_idea_container_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -57,7 +58,7 @@ class PiContainerApi implements TokenContainerApi {
Future<ContainerSyncUpdates?> sync(TokenContainerFinalized container, TokenState tokenState) async {
final containerTokenTemplates = tokenState.containerTokens(container.serial).toTemplates();

final maybePiTokensTemplates = (container.policies.initialTokenTransfer) ? tokenState.maybePiTokens.toTemplates() : <TokenTemplate>[];
final notLinkedTokenTemplates = (container.policies.initialTokenTransfer) ? tokenState.notLinkedTokens.toTemplates() : <TokenTemplate>[];

final ContainerChallenge challenge = await _getChallenge(container, container.syncUrl);

Expand All @@ -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
],
);

Expand All @@ -91,7 +92,7 @@ class PiContainerApi implements TokenContainerApi {

// MaybePiTokens Should not be deleted
final mergedTemplatesWithOtps = _handleMaybePiTokens(
maybePiTokensTemplates: maybePiTokensTemplates,
maybePiTokensTemplates: notLinkedTokenTemplates,
serverTokensWithOtps: serverTokensWithOtps,
container: container,
);
Expand Down Expand Up @@ -194,8 +195,29 @@ class PiContainerApi implements TokenContainerApi {
@override
Future<bool> 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<UnregisterContainerResultValue>();
final errorResponse = piResponse?.asError;
if (errorResponse != null) throw errorResponse.piServerResultError;
if (response.statusCode != 200 || piResponse == null) throw ResponseError(response);

return piResponse.asSuccess!.resultValue.success;
}

/* //////////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>'),
};
}
Expand Down Expand Up @@ -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<String, dynamic> json) {
final map = validateMap(
map: json,
validators: {
CONTAINER_UNREGISTER_SUCCESS: const ObjectValidator<bool>(),
},
name: 'UnregisterContainerResultValue#fromJson',
);
return UnregisterContainerResultValue(
success: map[CONTAINER_UNREGISTER_SUCCESS] as bool,
);
}
}
6 changes: 3 additions & 3 deletions lib/model/extensions/token_folder_extension.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ extension TokenListExtension on List<Token> {
return nonPiTokens;
}

List<Token> get maybePiTokens {
final maybePiTokens = where((token) => token.isPrivacyIdeaToken == null).toList();
Logger.debug('${maybePiTokens.length}/$length tokens with "isPrivacyIdeaToken == null"');
List<Token> 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;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/model/pi_server_response.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,6 @@ class PiServerResponse<T extends PiServerResultValue> with _$PiServerResponse {
}

factory PiServerResponse.fromResponse(Response response) {
return PiServerResponse.fromJson(jsonDecode(response.body));
return PiServerResponse<T>.fromJson(jsonDecode(response.body));
}
}
5 changes: 3 additions & 2 deletions lib/model/riverpod_states/token_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class TokenState {
List<PushToken> get pushTokensToRollOut =>
pushTokens.where((element) => !element.isRolledOut && element.rolloutState == PushTokenRollOutState.rolloutNotStarted).toList();

List<Token> get maybePiTokens => tokens.maybePiTokens;
/// Tokens that are not in a container but possibly can
List<Token> get notLinkedTokens => tokens.notLinkedTokens;

const TokenState({
required this.tokens,
Expand All @@ -54,7 +55,7 @@ class TokenState {
lastlyDeletedTokens = lastlyDeletedTokens ?? const [];

List<Token> 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;
}
Expand Down
4 changes: 2 additions & 2 deletions lib/model/tokens/day_password_token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class DayPasswordToken extends OTPToken {
OTP_AUTH_ISSUER: const ObjectValidatorNullable<String>(),
OTP_AUTH_SERIAL: const ObjectValidatorNullable<String>(),
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<String>(),
Expand Down Expand Up @@ -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) |
Expand Down
4 changes: 2 additions & 2 deletions lib/model/tokens/hotp_token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ class HOTPToken extends OTPToken {
OTP_AUTH_ISSUER: const ObjectValidatorNullable<String>(),
OTP_AUTH_SERIAL: const ObjectValidatorNullable<String>(),
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<String>(),
OTP_AUTH_PIN: boolValidatorNullable,
},
Expand Down
2 changes: 1 addition & 1 deletion lib/model/tokens/push_token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
Expand Down
8 changes: 4 additions & 4 deletions lib/model/tokens/totp_token.dart
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ class TOTPToken extends OTPToken {
OTP_AUTH_ISSUER: const ObjectValidatorNullable<String>(),
OTP_AUTH_SERIAL: const ObjectValidatorNullable<String>(),
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<String>(),
OTP_AUTH_PIN: boolValidatorNullable,
},
Expand Down Expand Up @@ -176,9 +176,9 @@ class TOTPToken extends OTPToken {
OTP_AUTH_ISSUER: const ObjectValidator<String>(defaultValue: ''),
OTP_AUTH_SERIAL: const ObjectValidatorNullable<String>(),
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<String>(),
OTP_AUTH_PIN: boolValidatorNullable,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ Future<String?>? _parse2StepSecret(Uri uri) {
map: queryParameters,
validators: {
OTP_AUTH_SECRET_BASE32: ObjectValidator<Uint8List>(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',
);
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/identifiers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
7 changes: 5 additions & 2 deletions lib/utils/object_validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,11 @@ final otpAuthCounterValidator = ObjectValidator<int>(
allowedValues: (v) => v >= 0,
);

final stringToIntValidatorNullable = stringToIntvalidator.nullable();
final stringToIntvalidator = ObjectValidator<int>(transformer: (v) => int.parse(v));
final intValidatorNullable = intValidator.nullable();
final intValidator = ObjectValidator<int>(transformer: (v) {
if (v is int) return v;
return int.parse(v);
});

final intToStringValidator = ObjectValidator<String>(transformer: (v) => (v as int).toString());
final intToStringValidatorNullable = intToStringValidator.nullable();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,9 +273,14 @@ class TokenContainerNotifier extends _$TokenContainerNotifier with ResultHandler

// DELETE CONTAINER

Future<TokenContainerState> unregisterDelete(TokenContainerFinalized container) {
final unregisterd = _containerApi.unregister(container);
throw UnimplementedError();
Future<TokenContainerState> 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<TokenContainerState> deleteContainer(TokenContainer container) async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class DeleteContainerAction extends ConsumerSlideableAction {

Future<void> _showDeleteDialog(BuildContext context, WidgetRef ref) => showDialog(
context: context,
useRootNavigator: false,
builder: (context) => DeleteContainerDialog(container),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},
Expand All @@ -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<void> _deleteContainer(WidgetRef ref) {
if (container is TokenContainerFinalized) {
return ref.read(tokenContainerProvider.notifier).unregisterDelete(container as TokenContainerFinalized);
} else {
return ref.read(tokenContainerProvider.notifier).deleteContainer(container);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,21 @@ class _TransferDeleteContainerDialogState extends ConsumerState<TransferDeleteCo
return switch (isUnlinked) {
null => 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ActionTheme>()!.deleteColor,
backgroundColor: isEnabled ? Theme.of(context).extension<ActionTheme>()!.deleteColor : Theme.of(context).extension<ActionTheme>()!.disabledColor,
foregroundColor: Theme.of(context).extension<ActionTheme>()!.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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,17 @@ class _TokenWidgetBaseState extends ConsumerState<TokenWidgetBase> {

tokenDeleteable.then((value) {
setState(() {
tokenIsDeleteable = value ?? false;
tokenIsDeleteable = value ?? true;
});
});

final List<ConsumerSlideableAction> 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)) {
Expand Down

0 comments on commit 7242c32

Please sign in to comment.