diff --git a/.github/workflows/at_libraries.yaml b/.github/workflows/at_libraries.yaml index 1e7c6d5a..7f17a861 100644 --- a/.github/workflows/at_libraries.yaml +++ b/.github/workflows/at_libraries.yaml @@ -26,7 +26,7 @@ jobs: - at_contact - at_server_status steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 # v1.6.5 with: @@ -66,7 +66,7 @@ jobs: - at_commons - at_utils steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 # v1.6.5 with: @@ -107,7 +107,7 @@ jobs: - at_onboarding_cli_functional_tests steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: dart-lang/setup-dart@0a8a0fc875eb934c15d08629302413c671d3f672 # v1.6.5 with: diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 5c03af26..7b0990bc 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -22,6 +22,6 @@ jobs: egress-policy: audit - name: 'Checkout Repository' - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: 'Dependency Review' uses: actions/dependency-review-action@5a2ce3f5b92ee19cbb1541a4984c76d921601d7c # v4.3.4 diff --git a/.github/workflows/melos_bootstrap.yaml b/.github/workflows/melos_bootstrap.yaml index 0ea0132b..72a370b4 100644 --- a/.github/workflows/melos_bootstrap.yaml +++ b/.github/workflows/melos_bootstrap.yaml @@ -8,7 +8,7 @@ jobs: melos-bootstrap: runs-on: ubuntu-latest steps: - - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: subosito/flutter-action@44ac965b96f18d999802d4b807e3256d5a3f9fa1 # v2.16.0 with: channel: "stable" diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index d313ba40..cabd79d6 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -32,7 +32,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 with: name: SARIF file path: results.sarif @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@6db8d6351fd0be61f9ed8ebd12ccd35dcec51fea # v3.26.11 + uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13 with: sarif_file: results.sarif diff --git a/packages/at_cli_commons/CHANGELOG.md b/packages/at_cli_commons/CHANGELOG.md index 00db432b..61a5d47b 100644 --- a/packages/at_cli_commons/CHANGELOG.md +++ b/packages/at_cli_commons/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.2.0 +- feat: Add `standardAtClientStoragePath` and `standardAtClientStorageDir` + to utils.dart + ## 1.1.0 - feat: Add `maxConnectAttempts` parameter to CLIBase. The default is 20, diff --git a/packages/at_cli_commons/lib/src/cli_base.dart b/packages/at_cli_commons/lib/src/cli_base.dart index 12313333..bb57aca0 100644 --- a/packages/at_cli_commons/lib/src/cli_base.dart +++ b/packages/at_cli_commons/lib/src/cli_base.dart @@ -19,27 +19,31 @@ class CLIBase { ..addOption('atsign', abbr: 'a', mandatory: true, help: 'This client\'s atSign') ..addOption('namespace', abbr: 'n', mandatory: true, help: 'Namespace') - ..addOption('key-file', - abbr: 'k', - mandatory: false, + ..addOption( + 'key-file', + abbr: 'k', + mandatory: false, help: 'Your atSign\'s atKeys file if not in ~/.atsign/keys/') ..addOption('cram-secret', abbr: 'c', mandatory: false, help: 'atSign\'s cram secret') ..addOption('home-dir', abbr: 'h', mandatory: false, help: 'home directory') - ..addOption('storage-dir', - abbr: 's', - mandatory: false, + ..addOption( + 'storage-dir', + abbr: 's', + mandatory: false, help: 'directory for this client\'s local storage files') - ..addOption('root-domain', - abbr: 'd', - mandatory: false, - help: 'Root Domain', + ..addOption( + 'root-domain', + abbr: 'd', + mandatory: false, + help: 'Root Domain', defaultsTo: 'root.atsign.org') ..addFlag('verbose', abbr: 'v', negatable: false, help: 'More logging') ..addFlag('never-sync', negatable: false, help: 'Do not run sync') - ..addOption('max-connect-attempts', - help: 'Number of times to attempt to initially connect to atServer.' - ' Note: there is a 3-second delay between connection attempts.', + ..addOption( + 'max-connect-attempts', + help: 'Number of times to attempt to initially connect to atServer.' + ' Note: there is a 3-second delay between connection attempts.', defaultsTo: defaultMaxConnectAttempts.toString()); /// Constructs a CLIBase from a list of command-line arguments @@ -149,11 +153,19 @@ class CLIBase { } atKeysFilePathToUse = - atKeysFilePath ?? '$homeDir/.atsign/keys/${this.atSign}_key.atKeys'; - localStoragePathToUse = - storageDir ?? '$homeDir/.$nameSpace/${this.atSign}/storage'; + (atKeysFilePath ?? '$homeDir/.atsign/keys/${this.atSign}_key.atKeys') + .replaceAll('/', Platform.pathSeparator); + localStoragePathToUse = (storageDir ?? + standardAtClientStoragePath( + baseDir: homeDir!, + atSign: this.atSign, + progName: nameSpace, + uniqueID: 'single', + )) + .replaceAll('/', Platform.pathSeparator); downloadPathToUse = - downloadDir ?? '$homeDir/.$nameSpace/${this.atSign}/files'; + (downloadDir ?? '$homeDir!/.atsign/downloads/${this.atSign}/$nameSpace') + .replaceAll('/', Platform.pathSeparator); AtSignLogger.defaultLoggingHandler = AtSignLogger.stdErrLoggingHandler; @@ -181,7 +193,7 @@ class CLIBase { ..namespace = nameSpace ..downloadPath = downloadPathToUse ..isLocalStoreRequired = true - ..commitLogPath = '$localStoragePathToUse/commitLog' + ..commitLogPath = '$localStoragePathToUse/commitLog'.replaceAll('/', Platform.pathSeparator) ..rootDomain = rootDomain ..fetchOfflineNotifications = true ..atKeysFilePath = atKeysFilePathToUse diff --git a/packages/at_cli_commons/lib/src/utils.dart b/packages/at_cli_commons/lib/src/utils.dart index f3e5f758..1001d8ee 100644 --- a/packages/at_cli_commons/lib/src/utils.dart +++ b/packages/at_cli_commons/lib/src/utils.dart @@ -1,5 +1,43 @@ import 'dart:io'; import 'package:at_client/at_client.dart'; +import 'package:path/path.dart' as path; + +String standardAtClientStoragePath({ + required String baseDir, + required String atSign, + required String progName, // e.g. npt, sshnp, sshnpd, srvd etc + String uniqueID = 'single', +}) { + return path.normalize('$baseDir' + '/.atsign' + '/storage' + '/$atSign' + '/.$progName' + '/$uniqueID' + .replaceAll('/', Platform.pathSeparator)); +} + +Directory standardAtClientStorageDir({ + required String atSign, + required String progName, // e.g. npt, sshnp, sshnpd, srvd etc + required String uniqueID, +}) { + if (Platform.isWindows) { + return Directory(standardAtClientStoragePath( + baseDir: Platform.environment['TEMP']!, + atSign: atSign, + progName: progName, + uniqueID: uniqueID, + )); + } else { + return Directory(standardAtClientStoragePath( + baseDir: getHomeDirectory()!, + atSign: atSign, + progName: progName, + uniqueID: uniqueID, + )); + } +} /// Get the home directory or null if unknown. String? getHomeDirectory({bool throwIfNull = false}) { diff --git a/packages/at_cli_commons/pubspec.yaml b/packages/at_cli_commons/pubspec.yaml index 6fdc7faa..8e5205cf 100644 --- a/packages/at_cli_commons/pubspec.yaml +++ b/packages/at_cli_commons/pubspec.yaml @@ -1,6 +1,6 @@ name: at_cli_commons description: Library of useful stuff when building cli programs which use the AtClient SDK -version: 1.1.0 +version: 1.2.0 repository: https://github.com/atsign-foundation/at_libraries/tree/trunk/packages/at_cli_commons homepage: https://docs.atsign.com/ @@ -17,6 +17,7 @@ dependencies: version: ^3.0.2 logging: ^1.2.0 meta: ^1.11.0 + path: ^1.9.0 dev_dependencies: lints: ^3.0.0 diff --git a/packages/at_commons/CHANGELOG.md b/packages/at_commons/CHANGELOG.md index 38acb509..667c6b02 100644 --- a/packages/at_commons/CHANGELOG.md +++ b/packages/at_commons/CHANGELOG.md @@ -1,3 +1,5 @@ +## 5.0.1 +- fix: export regex utils class ## 5.0.0 - [Breaking Change]feat: Emit the isEncrypted value in the metadata if it is false - fix: update pkam regex to accept sha512 as hashing algo diff --git a/packages/at_commons/lib/at_commons.dart b/packages/at_commons/lib/at_commons.dart index cecbd664..871c3b02 100644 --- a/packages/at_commons/lib/at_commons.dart +++ b/packages/at_commons/lib/at_commons.dart @@ -29,6 +29,7 @@ export 'package:at_commons/src/verb/verb_util.dart'; export 'package:at_commons/src/auth/auth_mode.dart'; export 'package:at_commons/src/verb/enroll_params.dart'; export 'package:at_commons/src/enroll/enrollment.dart'; +export 'package:at_commons/src/utils/at_key_regex_utils.dart'; @experimental export 'package:at_commons/src/telemetry/at_telemetry.dart'; export 'package:at_commons/src/utils/string_utils.dart'; diff --git a/packages/at_commons/pubspec.yaml b/packages/at_commons/pubspec.yaml index 19435cae..2ad37a38 100644 --- a/packages/at_commons/pubspec.yaml +++ b/packages/at_commons/pubspec.yaml @@ -1,6 +1,6 @@ name: at_commons description: A library of Dart and Flutter utility classes that are used across other components of the atPlatform. -version: 5.0.0 +version: 5.0.1 repository: https://github.com/atsign-foundation/at_libraries homepage: https://atsign.dev diff --git a/packages/at_onboarding_cli/CHANGELOG.md b/packages/at_onboarding_cli/CHANGELOG.md index f8a46688..b16000ce 100644 --- a/packages/at_onboarding_cli/CHANGELOG.md +++ b/packages/at_onboarding_cli/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.8.0 +- feat: add `unrevoke` command to the activate CLI +- feat: add `delete` command to the activate CLI +## 1.7.0 +- feat: add `auto` command to the activate CLI ## 1.6.4 - build[deps]: upgrade: \ at_client to 3.2.2 | at_commons to 5.0.0 | at_lookup to 3.0.49 | at_utils to 3.0.19 \ diff --git a/packages/at_onboarding_cli/lib/src/activate_cli/activate_cli.dart b/packages/at_onboarding_cli/lib/src/activate_cli/activate_cli.dart index e7207ef2..128d499e 100644 --- a/packages/at_onboarding_cli/lib/src/activate_cli/activate_cli.dart +++ b/packages/at_onboarding_cli/lib/src/activate_cli/activate_cli.dart @@ -6,11 +6,13 @@ import 'package:at_onboarding_cli/at_onboarding_cli.dart'; import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart'; import 'package:at_utils/at_logger.dart'; +@Deprecated('Use auth_cli') Future main(List arguments) async { int exitCode = await wrappedMain(arguments); exit(exitCode); } +@Deprecated('Use auth_cli') Future wrappedMain(List arguments) async { //defaults String rootServer = 'root.atsign.org'; @@ -57,6 +59,7 @@ Future wrappedMain(List arguments) async { return await activate(argResults); } +@Deprecated('Use auth_cli') Future activate(ArgResults argResults, {AtOnboardingService? atOnboardingService}) async { stdout.writeln('[Information] Root server is ${argResults['rootServer']}'); diff --git a/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart b/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart index e01cb5bc..89f56677 100644 --- a/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart +++ b/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -5,6 +6,7 @@ import 'package:args/args.dart'; import 'package:at_auth/at_auth.dart'; import 'package:at_cli_commons/at_cli_commons.dart'; import 'package:at_client/at_client.dart'; +import 'package:at_commons/at_builders.dart'; import 'package:at_lookup/at_lookup.dart'; import 'package:at_onboarding_cli/at_onboarding_cli.dart'; import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart'; @@ -20,10 +22,25 @@ final AtSignLogger logger = AtSignLogger(' CLI '); final aca = AuthCliArgs(); +Directory? storageDir; + +void deleteStorage() { + // Windows will not let us delete files that are open + // so will will ignore this step and leave them in %localappdata%\Temp + if (!Platform.isWindows) { + if (storageDir != null) { + if (storageDir!.existsSync()) { + // stderr.writeln('${DateTime.now()} : Cleaning up temporary files'); + storageDir!.deleteSync(recursive: true); + } + } + } +} + Future main(List arguments) async { AtSignLogger.defaultLoggingHandler = AtSignLogger.stdErrLoggingHandler; try { - return await _main(arguments); + return await wrappedMain(arguments); } on ArgumentError catch (e) { stderr.writeln('Invalid argument: ${e.message}'); aca.parser.printAllCommandsUsage(); @@ -32,10 +49,14 @@ Future main(List arguments) async { stderr.writeln('Error: $e'); aca.parser.printAllCommandsUsage(); return 1; + } finally { + try { + deleteStorage(); + } catch (_) {} } } -Future _main(List arguments) async { +Future wrappedMain(List arguments) async { if (arguments.isEmpty) { stderr.writeln('You must supply a command.'); aca.parser.printAllCommandsUsage(showSubCommandParams: false); @@ -157,6 +178,10 @@ Future _main(List arguments) async { await approve( commandArgResults, await createAtClient(commandArgResults)); + case AuthCliCommand.auto: + await autoApprove( + commandArgResults, await createAtClient(commandArgResults)); + case AuthCliCommand.deny: await deny(commandArgResults, await createAtClient(commandArgResults)); @@ -171,6 +196,14 @@ Future _main(List arguments) async { // Write keys to @atSign_keys.atKeys IFF it doesn't already exist; if // it does exist, then write to @atSign_appName_deviceName_keys.atKeys await enroll(commandArgResults); + + case AuthCliCommand.unrevoke: + await unrevoke( + commandArgResults, await createAtClient(commandArgResults)); + + case AuthCliCommand.delete: + await deleteEnrollment( + commandArgResults, await createAtClient(commandArgResults)); } } on ArgumentError catch (e) { stderr @@ -248,16 +281,21 @@ Future status(ArgResults ar) async { } Future createAtClient(ArgResults ar) async { - String nameSpace = 'at_auth_cli'; + String nameSpace = 'at_activate'; String atSign = AtUtils.fixAtSign(ar[AuthCliArgs.argNameAtSign]); + storageDir = standardAtClientStorageDir( + atSign: atSign, + progName: nameSpace, + uniqueID: '${DateTime.now().millisecondsSinceEpoch}', + ); + CLIBase cliBase = CLIBase( atSign: atSign, atKeysFilePath: ar[AuthCliArgs.argNameAtKeys], nameSpace: nameSpace, rootDomain: ar[AuthCliArgs.argNameAtDirectoryFqdn], homeDir: getHomeDirectory(), - storageDir: '${getHomeDirectory()}/.atsign/$nameSpace/$atSign/storage' - .replaceAll('/', Platform.pathSeparator), + storageDir: storageDir!.path, verbose: ar[AuthCliArgs.argNameVerbose] || ar[AuthCliArgs.argNameDebug], syncDisabled: true, maxConnectAttempts: int.parse( @@ -284,24 +322,27 @@ Future onboard(ArgResults argResults, {AtOnboardingService? svc}) async { '[Information] Onboarding your atSign. This may take up to 2 minutes.'); try { await svc.onboard(); - logger.finest('svc.onboard() has returned - will exit(0)'); - exit(0); + return; } on InvalidDataException catch (e) { - stderr.writeln( - '[Error] Onboarding failed. Invalid data provided by user. Please try again\nCause: ${e.message}'); - exit(1); + throw AtEnrollmentException( + 'Onboarding failed. Please try again. Cause: ${e.message}'); } on InvalidRequestException catch (e) { - stderr.writeln( - '[Error] Onboarding failed. Invalid data provided by user. Please try again\nCause: ${e.message}'); - exit(1); - } on AtActivateException catch (e) { - stderr.writeln('[Error] ${e.message}'); - exit(1); - } on Exception catch (e) { - stderr.writeln( - '[Error] Onboarding failed. It looks like something went wrong on our side.\n' - 'Please try again or contact support@atsign.com\nCause: $e'); - exit(1); + throw AtEnrollmentException( + 'Onboarding failed. Please try again. Cause: ${e.message}'); + } on AtActivateException { + rethrow; + } catch (e) { + throw ('Onboarding failed.' + ' It looks like something went wrong on our side.' + ' Please try again or contact support@atsign.com\nCause: $e'); + } +} + +String parseServerResponse(String? response) { + if (response != null && response.startsWith('data:')) { + return response.replaceFirst('data:', ''); + } else { + throw ('Unexpected server response: $response'); } } @@ -494,11 +535,20 @@ Future interactive(ArgResults argResults, AtClient atClient) async { case AuthCliCommand.approve: await approve(commandArgResults, atClient); + case AuthCliCommand.auto: + await autoApprove(commandArgResults, atClient); + case AuthCliCommand.deny: await deny(commandArgResults, atClient); case AuthCliCommand.revoke: await revoke(commandArgResults, atClient); + + case AuthCliCommand.unrevoke: + await unrevoke(commandArgResults, atClient); + + case AuthCliCommand.delete: + await deleteEnrollment(commandArgResults, atClient); } } on ArgumentError catch (e) { stderr.writeln( @@ -554,8 +604,7 @@ Future _list( stdout.writeln("Found ${filtered.length} matching enrollment records"); return filtered; } else { - stderr.writeln('Exiting: Unexpected server response: $rawResponse'); - exit(1); + throw Exception('Unexpected server response: $rawResponse'); } } @@ -587,20 +636,14 @@ Future list(ArgResults ar, AtClient atClient) async { } Future _fetch(String eId, AtLookUp atLookup) async { - String rawResponse = (await atLookup.executeCommand( - 'enroll:fetch:' - '{"enrollmentId":"$eId"}' - '\n', - auth: true))!; - - if (rawResponse.startsWith('data:')) { - rawResponse = rawResponse.substring(rawResponse.indexOf('data:') + 5); - // response is a Map - return jsonDecode(rawResponse); - } else { - stderr.writeln('Exiting: Unexpected server response: $rawResponse'); - exit(1); - } + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.fetch + ..enrollmentId = eId; + String? response = await atLookup.executeVerb(enrollVerbBuilder); + + response = parseServerResponse(response); + // response is a Map + return jsonDecode(response); } Future fetch(ArgResults argResults, AtClient atClient) async { @@ -650,7 +693,8 @@ Future _fetchOrListAndFilter( return enrollmentMap; } -Future approve(ArgResults ar, AtClient atClient) async { +Future approve(ArgResults ar, AtClient atClient, {int? limit}) async { + int approved = 0; AtLookUp atLookup = atClient.getRemoteSecondary()!.atLookUp; Map toApprove = await _fetchOrListAndFilter( @@ -663,13 +707,15 @@ Future approve(ArgResults ar, AtClient atClient) async { if (toApprove.isEmpty) { stderr.writeln('No matching enrollment(s) found'); - return; + return approved; } // Iterate through the requests, approve each one for (String eId in toApprove.keys) { Map er = toApprove[eId]; - stdout.writeln('Approving enrollmentId $eId'); + stdout.writeln('Approving enrollmentId $eId' + ' with appName "${er['appName']}"' + ' and deviceName "${er['deviceName']}"'); // Then make a 'decision' object using data from the enrollment request EnrollmentRequestDecision decision = EnrollmentRequestDecision.approved( ApprovedRequestDecisionBuilder( @@ -680,9 +726,119 @@ Future approve(ArgResults ar, AtClient atClient) async { final response = await atAuthBase .atEnrollment(atClient.getCurrentAtSign()!) .approve(decision, atLookup); - // 'enroll:approve:{"enrollmentId":"$enrollmentId"}' + stdout.writeln('Server response: $response'); + + approved++; + + if (limit != null && approved >= limit) { + return approved; + } + } + return approved; +} + +Future autoApprove(ArgResults ar, AtClient atClient) async { + int approved = 0; + int limit = int.parse(ar[AuthCliArgs.argNameLimit]); + String? arx = ar[AuthCliArgs.argNameAppNameRegex]; + String? drx = ar[AuthCliArgs.argNameDeviceNameRegex]; + bool approveExisting = ar[AuthCliArgs.argNameAutoApproveExisting]; + + if (arx == null && drx == null) { + throw IllegalArgumentException( + 'You must supply ${AuthCliArgs.argNameAppNameRegex}' + ' and/or ${AuthCliArgs.argNameDeviceNameRegex}'); + } + + if (approveExisting) { + // Start by approving any which match and are already there + stdout.writeln('Approving any requests already there which are a match'); + approved = await approve(ar, atClient, limit: limit); + stdout.writeln(); + } + + // If we've already approved our limit then we're done + if (approved >= limit) { + return approved; + } + + Completer completer = Completer(); + + RegExp? appRegex; + RegExp? deviceRegex; + if (arx != null) { + appRegex = RegExp(arx); } + if (drx != null) { + deviceRegex = RegExp(drx); + } + + AtLookUp atLookup = atClient.getRemoteSecondary()!.atLookUp; + + // listen for enrollment requests + stdout.writeln('Listening for new enrollment requests'); + + final stream = atClient.notificationService.subscribe( + regex: r'.*\.new\.enrollments\.__manage', shouldDecrypt: false); + + final subscription = stream.listen((AtNotification n) async { + if (completer.isCompleted) { + return; // Don't handle any more if we're already done + } + + String eId = n.key.substring(0, n.key.indexOf('.')); + + final er = jsonDecode(n.value!); + stdout.writeln('Got enrollment request ID $eId' + ' with appName "${er['appName']}"' + ' and deviceName "${er['deviceName']}"'); + + // check the request matches our params + String appName = er['appName']; + String deviceName = er['deviceName']; + if ((appRegex?.hasMatch(appName) ?? true) && + (deviceRegex?.hasMatch(deviceName) ?? true)) { + // request matched, let's approve it + stdout.writeln('Approving enrollment request' + ' which matched the regex filters' + ' (app: "$arx" and device: "$drx" respectively)'); + + EnrollmentRequestDecision decision = EnrollmentRequestDecision.approved( + ApprovedRequestDecisionBuilder( + enrollmentId: eId, + encryptedAPKAMSymmetricKey: er['encryptedAPKAMSymmetricKey'])); + + // Finally call approve method via an AtEnrollment object + final response = await atAuthBase + .atEnrollment(atClient.getCurrentAtSign()!) + .approve(decision, atLookup); + stdout.writeln('Approval successful.\n' + '\tResponse: $response'); + + // increment approved count + approved++; + + // check approved vs limit + if (approved >= limit) { + // if reached limit, complete the future + stdout + .writeln('Approved $approved requests - limit was $limit - done.'); + completer.complete(); + } + } else { + stdout.writeln('Ignoring enrollment request' + ' which does not match the regex filters' + ' (app: "$arx" and device: "$drx" respectively)'); + } + stdout.writeln(); + }); + + // await future then cancel the subscription + await completer.future; + await subscription.cancel(); + + return approved; } Future deny(ArgResults ar, AtClient atClient) async { @@ -704,9 +860,10 @@ Future deny(ArgResults ar, AtClient atClient) async { // Iterate through the requests, deny each one for (String eId in toDeny.keys) { stdout.writeln('Denying enrollmentId $eId'); - // 'enroll:deny:{"enrollmentId":"$enrollmentId"}' - String? response = await atLookup - .executeCommand('enroll:deny:{"enrollmentId":"$eId"}\n', auth: true); + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.deny + ..enrollmentId = eId; + String? response = await atLookup.executeVerb(enrollVerbBuilder); stdout.writeln('Server response: $response'); } } @@ -737,6 +894,43 @@ Future revoke(ArgResults ar, AtClient atClient) async { } } +Future unrevoke(ArgResults ar, AtClient atClient) async { + AtLookUp atLookup = atClient.getRemoteSecondary()!.atLookUp; + + Map toUnRevoke = await _fetchOrListAndFilter( + atLookup, + EnrollmentStatus.approved.name, // must be status approved + eId: ar[AuthCliArgs.argNameEnrollmentId], + arx: ar[AuthCliArgs.argNameAppNameRegex], + drx: ar[AuthCliArgs.argNameDeviceNameRegex], + ); + + if (toUnRevoke.isEmpty) { + stderr.writeln('No matching enrollment(s) found'); + return; + } + + for (String eId in toUnRevoke.keys) { + stdout.writeln('Un-Revoking enrollmentId $eId'); + String? response = await atLookup.executeCommand( + 'enroll:unrevoke:{"enrollmentId":"$eId"}\n', + auth: true); + stdout.writeln('Server response: $response'); + } +} + +Future deleteEnrollment(ArgResults ar, AtClient atClient) async { + AtLookUp atLookup = atClient.getRemoteSecondary()!.atLookUp; + String eId = ar[AuthCliArgs.argNameEnrollmentId]; + EnrollVerbBuilder enrollVerbBuilder = EnrollVerbBuilder() + ..enrollmentId = eId + ..operation = EnrollOperationEnum.delete; + stdout.writeln('Sending delete request'); + String? response = await atLookup.executeVerb(enrollVerbBuilder); + response = parseServerResponse(response); + stdout.writeln('Server response: $response'); +} + @visibleForTesting AtOnboardingService createOnboardingService(ArgResults ar) { String atSign = AtUtils.fixAtSign(ar[AuthCliArgs.argNameAtSign]); diff --git a/packages/at_onboarding_cli/lib/src/cli/auth_cli_args.dart b/packages/at_onboarding_cli/lib/src/cli/auth_cli_args.dart index c47570bf..445dff7c 100644 --- a/packages/at_onboarding_cli/lib/src/cli/auth_cli_args.dart +++ b/packages/at_onboarding_cli/lib/src/cli/auth_cli_args.dart @@ -44,8 +44,16 @@ enum AuthCliCommand { list(usage: 'List enrollment requests'), fetch(usage: 'Fetch a specific enrollment request'), approve(usage: 'Approve a pending enrollment request'), + auto( + usage: 'Listen for new enrollment requests which match the parameters' + ' supplied, and auto-approve them. Will exit after N (defaults to 1)' + ' enrollment requests have been approved.'), deny(usage: 'Deny a pending enrollment request'), revoke(usage: 'Revoke approval of a previously-approved enrollment'), + unrevoke(usage: 'Restores access to the previously revoked enrollment'), + delete( + usage: 'Deletes an enrollment. Requires an enrollmentId to be provided' + '\nNOTE: Can ONLY delete denied and revoked enrollments'), enroll( usage: 'Enroll is used when a program needs to authenticate and' ' "atKeys" are not available, and "onboard" has already been run' @@ -93,9 +101,15 @@ class AuthCliArgs { static const argNameEnrollmentId = 'enrollmentId'; static const argNameEnrollmentStatus = 'enrollmentStatus'; static const argNameAppNameRegex = 'arx'; + static const argAbbrAppNameRegex = 'A'; static const argNameDeviceNameRegex = 'drx'; + static const argAbbrDeviceNameRegex = 'D'; + static const argNameLimit = 'limit'; + static const argAbbrLimit = 'L'; static const argNameMaxConnectAttempts = 'mca'; static const argNameExpiry = 'expiry'; + static const argAbbrExpiry = 'e'; + static const argNameAutoApproveExisting = 'approve-existing'; ArgParser get parser { return _aap; @@ -165,11 +179,20 @@ class AuthCliArgs { case AuthCliCommand.approve: return createApproveCommandParser(); + case AuthCliCommand.auto: + return createAutoApproveCommandParser(); + case AuthCliCommand.deny: return createDenyCommandParser(); case AuthCliCommand.revoke: return createRevokeCommandParser(); + + case AuthCliCommand.unrevoke: + return createUnRevokeCommandParser(); + + case AuthCliCommand.delete: + return createDeleteCommandParser(); } } @@ -278,7 +301,7 @@ class AuthCliArgs { mandatory: true, ); p.addOption(argNameExpiry, - abbr: 'e', + abbr: argAbbrExpiry, help: 'The duration for which the SPP remains active. The time duration can be passed as "2d,1h,10m,20s,999ms" for 2 days 1 hour 10 minutes 20 seconds 999 milliseconds', mandatory: false); @@ -296,7 +319,7 @@ class AuthCliArgs { ArgParser createOtpCommandParser() { ArgParser p = createSharedArgParser(hide: true); p.addOption(argNameExpiry, - abbr: 'e', + abbr: argAbbrExpiry, help: 'The duration for which the OTP remains active. The time duration can be passed as "2d,1h,10m,20s,999ms" for 2 days 1 hour 10 minutes 20 seconds 999 milliseconds', mandatory: false); @@ -337,7 +360,7 @@ class AuthCliArgs { mandatory: true, ); p.addOption(argNameExpiry, - abbr: 'e', + abbr: argAbbrExpiry, help: 'The duration for which the APKAM keys remains active. The time duration can be passed as "2d,1h,10m,20s,999ms" for 2 days 1 hour 10 minutes 20 seconds 999 milliseconds', mandatory: false); @@ -356,8 +379,8 @@ class AuthCliArgs { allowed: EnrollmentStatus.values.map((c) => c.name).toList(), mandatory: false, ); - _addAppNameRegexOption(p); - _addDeviceNameRegexOption(p); + _addAppNameRegexOption(p, mandatory: false); + _addDeviceNameRegexOption(p, mandatory: false); return p; } @@ -369,28 +392,35 @@ class AuthCliArgs { return p; } - void _addAppNameRegexOption(ArgParser p) { + void _addAppNameRegexOption(ArgParser p, {required bool mandatory}) { p.addOption( argNameAppNameRegex, - help: 'Filter responses via regular expression on app name', - mandatory: false, + abbr: argAbbrAppNameRegex, + help: 'Filter requests via regular expression on app name', + mandatory: mandatory, ); } - void _addDeviceNameRegexOption(ArgParser p) { + void _addDeviceNameRegexOption(ArgParser p, {required bool mandatory}) { p.addOption( argNameDeviceNameRegex, - help: 'Filter responses via regular expression on device name', - mandatory: false, + abbr: argAbbrDeviceNameRegex, + help: 'Filter requests via regular expression on device name', + mandatory: mandatory, ); } - void _addEnrollmentIdOption(ArgParser p, {bool mandatory = false}) { + void _addEnrollmentIdOption( + ArgParser p, { + bool mandatory = false, + bool hide = false, + }) { p.addOption( argNameEnrollmentId, abbr: 'i', help: 'The ID of the enrollment request', mandatory: mandatory, + hide: hide, ); } @@ -399,8 +429,33 @@ class AuthCliArgs { ArgParser createApproveCommandParser() { ArgParser p = createSharedArgParser(hide: true); _addEnrollmentIdOption(p); - _addAppNameRegexOption(p); - _addDeviceNameRegexOption(p); + _addAppNameRegexOption(p, mandatory: false); + _addDeviceNameRegexOption(p, mandatory: false); + return p; + } + + /// auth approve + @visibleForTesting + ArgParser createAutoApproveCommandParser() { + ArgParser p = createSharedArgParser(hide: true); + _addEnrollmentIdOption(p, hide: true); + _addAppNameRegexOption(p, mandatory: true); + _addDeviceNameRegexOption(p, mandatory: true); + p.addOption( + argNameLimit, + abbr: argAbbrLimit, + help: 'Listen until this many requests have been approved', + mandatory: false, + defaultsTo: "1", + ); + p.addFlag( + argNameAutoApproveExisting, + help: 'Before starting to listen, approve any matching enrollment' + ' requests which already exist. Note: any approvals will count' + ' towards the limit.', + negatable: false, + defaultsTo: false, + ); return p; } @@ -409,8 +464,8 @@ class AuthCliArgs { ArgParser createDenyCommandParser() { ArgParser p = createSharedArgParser(hide: true); _addEnrollmentIdOption(p); - _addAppNameRegexOption(p); - _addDeviceNameRegexOption(p); + _addAppNameRegexOption(p, mandatory: false); + _addDeviceNameRegexOption(p, mandatory: false); return p; } @@ -419,8 +474,27 @@ class AuthCliArgs { ArgParser createRevokeCommandParser() { ArgParser p = createSharedArgParser(hide: true); _addEnrollmentIdOption(p); - _addAppNameRegexOption(p); - _addDeviceNameRegexOption(p); + _addAppNameRegexOption(p, mandatory: false); + _addDeviceNameRegexOption(p, mandatory: false); + return p; + } + + /// Restore the revoked enrollment Id. + @visibleForTesting + ArgParser createUnRevokeCommandParser() { + ArgParser p = createSharedArgParser(hide: true); + _addEnrollmentIdOption(p); + _addAppNameRegexOption(p, mandatory: false); + _addDeviceNameRegexOption(p, mandatory: false); + return p; + } + + /// auth delete denied enrollment: requires enrollmentId and atKeysFile path + /// requires the enrollment to be denied + @visibleForTesting + ArgParser createDeleteCommandParser() { + ArgParser p = createSharedArgParser(hide: true); + _addEnrollmentIdOption(p, mandatory: true); return p; } } diff --git a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart index 0d0c37cb..3db31f3a 100644 --- a/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart +++ b/packages/at_onboarding_cli/lib/src/onboard/at_onboarding_service_impl.dart @@ -31,6 +31,7 @@ class AtOnboardingServiceImpl implements AtOnboardingService { AtSignLogger logger = AtSignLogger('OnboardingCli'); AtOnboardingPreference atOnboardingPreference; AtLookUp? _atLookUp; + final _maxActivationRetries = 5; /// The object which controls what types of AtClients, NotificationServices /// and SyncServices get created when we call [AtClientManager.setCurrentAtSign]. @@ -321,15 +322,44 @@ class AtOnboardingServiceImpl implements AtOnboardingService { Duration retryInterval, { bool logProgress = true, }) async { + int retryAttempt = 1; while (true) { logger.info('Attempting pkam auth'); if (logProgress) { stderr.write('Checking ... '); } - bool pkamAuthSucceeded = await _attemptPkamAuth( - atLookUp, - enrollmentIdFromServer, - ); + bool pkamAuthSucceeded = false; + try { + // _attemptPkamAuth returns boolean value true when authentication is successful. + // Returns UnAuthenticatedException when authentication fails. + pkamAuthSucceeded = await atLookUp.pkamAuthenticate( + enrollmentId: enrollmentIdFromServer); + } on UnAuthenticatedException catch (e) { + // Error codes AT0401 and AT0026 indicate authentication failure due to unapproved enrollment. Retry until the enrollment is approved. + // The variable _pkamAuthSucceeded is false, allowing for PKAM authentication retries. + // Avoid checking "retryAttempt > _maxActivationRetries" here, as we want to continue retrying until enrollment is approved. + // The check for "retryAttempt > _maxActivationRetries" should only occur when the secondary server is unreachable due to network issues. + if (e.message.contains('error:AT0401') || + e.message.contains('error:AT0026')) { + logger.info('Pkam auth failed: ${e.message}'); + } + // Error code AT0025 represents Enrollment denied. Therefore, no need to retry; throw exception. + else if (e.message.contains('error:AT0025')) { + throw AtEnrollmentException( + 'The enrollment: $enrollmentIdFromServer is denied'); + } + } catch (e) { + String message = + 'Exception occurred when authenticating the atSign: $_atSign caused by ${e.toString()}'; + if (retryAttempt > _maxActivationRetries) { + message += ' Activation failed after $_maxActivationRetries attempts'; + logger.severe(message); + rethrow; + } + logger + .severe('$message. Attempting to retry for $retryAttempt attempt'); + retryAttempt++; + } if (pkamAuthSucceeded) { if (logProgress) { stderr.writeln(' approved.'); @@ -347,34 +377,6 @@ class AtOnboardingServiceImpl implements AtOnboardingService { } } - /// Try a single PKAM auth - Future _attemptPkamAuth(AtLookUp atLookUp, String enrollmentId) async { - try { - logger.finer('_attemptPkamAuth: Calling atLookUp.pkamAuthenticate'); - var pkamResult = - await atLookUp.pkamAuthenticate(enrollmentId: enrollmentId); - logger.finer( - '_attemptPkamAuth: atLookUp.pkamAuthenticate returned $pkamResult'); - if (pkamResult) { - return true; - } - } on UnAuthenticatedException catch (e) { - if (e.message.contains('error:AT0401') || - e.message.contains('error:AT0026')) { - logger.info('Pkam auth failed: ${e.message}'); - return false; - } else if (e.message.contains('error:AT0025')) { - throw AtEnrollmentException('enrollment denied'); - } - } catch (e) { - logger.shout('Unexpected exception: $e'); - rethrow; - } finally { - logger.finer('_attemptPkamAuth: complete'); - } - return false; - } - ///write newly created encryption keypairs into atKeys file Future _generateAtKeysFile( at_auth.AtAuthKeys atAuthKeys, { diff --git a/packages/at_onboarding_cli/lib/src/register_cli/register.dart b/packages/at_onboarding_cli/lib/src/register_cli/register.dart index 22fd5b41..fbb9bb9e 100644 --- a/packages/at_onboarding_cli/lib/src/register_cli/register.dart +++ b/packages/at_onboarding_cli/lib/src/register_cli/register.dart @@ -3,8 +3,7 @@ import 'dart:io'; import 'package:args/args.dart'; import 'package:at_client/at_client.dart'; -import 'package:at_onboarding_cli/src/activate_cli/activate_cli.dart' - as activate_cli; +import 'package:at_onboarding_cli/src/cli/auth_cli.dart' as auth_cli; import 'package:at_onboarding_cli/src/util/api_call_status.dart'; import 'package:at_onboarding_cli/src/util/at_onboarding_exceptions.dart'; import 'package:at_onboarding_cli/src/util/register_api_result.dart'; @@ -64,8 +63,14 @@ class Register { .add(ValidateOtp()) .start(); - await activate_cli - .wrappedMain(['-a', params['atsign']!, '-c', params['cramkey']!]); + await auth_cli.wrappedMain( + [ + '-a', + params['atsign']!, + '-c', + params['cramkey']!, + ], + ); } } diff --git a/packages/at_onboarding_cli/lib/src/util/onboarding_util.dart b/packages/at_onboarding_cli/lib/src/util/onboarding_util.dart index 5ef2b4bc..3a3dabb1 100644 --- a/packages/at_onboarding_cli/lib/src/util/onboarding_util.dart +++ b/packages/at_onboarding_cli/lib/src/util/onboarding_util.dart @@ -116,7 +116,7 @@ class OnboardingUtil { 'To register a new atSign to this email address, please log into the dashboard \'my.atsign.com/login\'.\n' 'Remove at least 1 atSign from your account and then try again.\n' 'Alternatively, you can retry this process with a different email address.'); - exit(1); + throw at_client.AtClientException.message(jsonDecoded['message']); } else { throw at_client.AtClientException.message( '${response.statusCode} ${jsonDecoded['message']}'); diff --git a/packages/at_onboarding_cli/pubspec.yaml b/packages/at_onboarding_cli/pubspec.yaml index 0315b95e..bfd75ae3 100644 --- a/packages/at_onboarding_cli/pubspec.yaml +++ b/packages/at_onboarding_cli/pubspec.yaml @@ -1,6 +1,6 @@ name: at_onboarding_cli description: Dart tools for initial client onboarding, subsequent client enrollment, and enrollment management. -version: 1.6.4 +version: 1.8.0 repository: https://github.com/atsign-foundation/at_libraries homepage: https://atsign.com documentation: https://docs.atsign.com/ @@ -28,7 +28,7 @@ dependencies: at_lookup: ^3.0.49 at_server_status: ^1.0.5 at_utils: ^3.0.19 - at_cli_commons: ^1.1.0 + at_cli_commons: ^1.2.0 at_persistence_secondary_server: ^3.0.64 duration: ^4.0.3 diff --git a/tests/at_onboarding_cli_functional_tests/test/at_onboarding_cli_test.dart b/tests/at_onboarding_cli_functional_tests/test/at_onboarding_cli_test.dart index 97709459..2fb7fb08 100644 --- a/tests/at_onboarding_cli_functional_tests/test/at_onboarding_cli_test.dart +++ b/tests/at_onboarding_cli_functional_tests/test/at_onboarding_cli_test.dart @@ -5,8 +5,7 @@ import 'package:at_client/at_client.dart'; import 'package:at_demo_data/at_demo_data.dart' as at_demos; import 'package:at_lookup/at_lookup.dart'; import 'package:at_onboarding_cli/at_onboarding_cli.dart'; -import 'package:at_onboarding_cli/src/activate_cli/activate_cli.dart' - as activate_cli; +import 'package:at_onboarding_cli/src/cli/auth_cli.dart' as auth_cli; import 'package:at_utils/at_utils.dart'; import 'package:test/test.dart'; @@ -180,7 +179,7 @@ void main() { 'vip.ve.atsign.zone' ]; // perform activation of atSign - await activate_cli.wrappedMain(args); + await auth_cli.wrappedMain(args); /// ToDo: test should NOT exit with status 0 after activation is complete /// Exiting with status 0 is ideal behaviour, but for the sake of the test we need to be diff --git a/tests/at_onboarding_cli_functional_tests/test/enrollment_cli_commands_test.dart b/tests/at_onboarding_cli_functional_tests/test/enrollment_cli_commands_test.dart new file mode 100644 index 00000000..5efe95eb --- /dev/null +++ b/tests/at_onboarding_cli_functional_tests/test/enrollment_cli_commands_test.dart @@ -0,0 +1,147 @@ +import 'dart:io'; + +import 'package:at_auth/at_auth.dart'; +import 'package:at_client/at_client.dart'; +import 'package:at_onboarding_cli/at_onboarding_cli.dart'; +import 'package:at_onboarding_cli/src/cli/auth_cli.dart' as auth_cli; +import 'package:at_utils/at_utils.dart'; +import 'package:test/test.dart'; + +void main() { + String atSign = '@sitaramđź› '; + String apkamKeysFilePath = 'storage/keys/@sitaram-apkam.atKeys'; + final logger = AtSignLogger('E2E Test'); + + group('A group of tests to validate enrollment commands', () { + /// The test verifies the following scenario's + /// 1. Onboards an atSign + /// 2. Sets Semi Permanent Passcode + /// 3. Submits an enrollment request + /// 4. Approves the enrollment request + /// 5. Performs authentication with the approved enrollment Id. Authentication should be successful. + /// 6. Revokes the enrollment Id. + /// 7. Performs authentication again with the revoked enrollment Id. Authentication fails this time. + /// 8. Unrevoke the enrollment Id. + /// 9. Performs authentication again with the unrevoked enrollment Id. Authentication should be successful. + test( + 'A test to verify end-to-end flow of approve revoke unrevoke of an enrollment', + () async { + AtOnboardingService atOnboardingService = AtOnboardingServiceImpl( + atSign, + getOnboardingPreference(atSign, + '${Platform.environment['HOME']}/.atsign/keys/${atSign}_key.atKeys') + // Fetched cram key from the at_demos repo. + ..cramSecret = + '15cdce8f92bcf7e742d5b75dc51ec06d798952f8bf7e8ff4c2b6448e5f7c2c12b570fe945f04011455fdc49cacdf9393d9c1ac4609ec71c1a0b0c213578e7ec7'); + + bool onboardingStatus = await atOnboardingService.onboard(); + expect(onboardingStatus, true); + // Set SPP + List args = [ + 'spp', + '-s', + 'ABC123', + '-a', + atSign, + '-r', + 'vip.ve.atsign.zone' + ]; + var res = await auth_cli.wrappedMain(args); + // Zero indicates successful completion. + expect(res, 0); + + // Submit enrollment request + AtEnrollmentResponse atEnrollmentResponse = await atOnboardingService + .sendEnrollRequest( + 'wavi', 'local-device', 'ABC123', {'e2etest': 'rw'}); + logger.info( + 'Submitted enrollment successfully with enrollmentId: ${atEnrollmentResponse.enrollmentId}'); + expect(atEnrollmentResponse.enrollStatus, EnrollmentStatus.pending); + expect(atEnrollmentResponse.enrollmentId.isNotEmpty, true); + + // Approve enrollment request + args = [ + 'approve', + '-a', + atSign, + '-r', + 'vip.ve.atsign.zone', + '-i', + atEnrollmentResponse.enrollmentId + ]; + res = await auth_cli.wrappedMain(args); + expect(res, 0); + logger.info( + 'Approved enrollment with enrollmentId: ${atEnrollmentResponse.enrollmentId}'); + + // Generate Atkeys file for the enrollment request. + await atOnboardingService.awaitApproval(atEnrollmentResponse); + await atOnboardingService.createAtKeysFile(atEnrollmentResponse, + atKeysFile: File(apkamKeysFilePath)); + + // Authenticate with APKAM keys + atOnboardingService = AtOnboardingServiceImpl( + atSign, getOnboardingPreference(atSign, apkamKeysFilePath)); + bool authResponse = await atOnboardingService.authenticate( + enrollmentId: atEnrollmentResponse.enrollmentId); + expect(authResponse, true); + + // Revoke the enrollment + args = [ + 'revoke', + '-a', + atSign, + '-r', + 'vip.ve.atsign.zone', + '-i', + atEnrollmentResponse.enrollmentId + ]; + res = await auth_cli.wrappedMain(args); + expect(res, 0); + logger.info( + 'Revoked enrollment with enrollmentId: ${atEnrollmentResponse.enrollmentId}'); + + // Perform authentication with revoked enrollmentId + expect( + () async => await atOnboardingService.authenticate( + enrollmentId: atEnrollmentResponse.enrollmentId), + throwsA(predicate((dynamic e) => e is AtAuthenticationException))); + + // UnRevoke the enrollment + args = [ + 'unrevoke', + '-a', + atSign, + '-r', + 'vip.ve.atsign.zone', + '-i', + atEnrollmentResponse.enrollmentId + ]; + res = await auth_cli.wrappedMain(args); + logger.info( + 'Un-Revoked enrollment with enrollmentId: ${atEnrollmentResponse.enrollmentId}'); + // Perform authentication with the unrevoked enrollment-id. + authResponse = await atOnboardingService.authenticate( + enrollmentId: atEnrollmentResponse.enrollmentId); + expect(authResponse, true); + }); + }); + + tearDown(() { + File file = File(apkamKeysFilePath); + file.deleteSync(); + }); +} + +AtOnboardingPreference getOnboardingPreference( + String atSign, String atKeysFilePath) { + atSign = AtUtils.fixAtSign(atSign); + AtOnboardingPreference atOnboardingPreference = AtOnboardingPreference() + ..namespace = 'buzz' + ..atKeysFilePath = atKeysFilePath + ..appName = 'buzz' + ..deviceName = 'iphone' + ..rootDomain = 'vip.ve.atsign.zone'; + + return atOnboardingPreference; +} diff --git a/tests/at_onboarding_cli_functional_tests/test/enrollment_test.dart b/tests/at_onboarding_cli_functional_tests/test/enrollment_test.dart index 8d6e9c7a..d458c8eb 100644 --- a/tests/at_onboarding_cli_functional_tests/test/enrollment_test.dart +++ b/tests/at_onboarding_cli_functional_tests/test/enrollment_test.dart @@ -237,10 +237,14 @@ void main() { var completer = Completer(); // Create a Completer //4. Subscribe to enrollment notifications; we will deny it when it arrives + String enrollmentId = ''; onboardingService_1.atClient!.notificationService .subscribe(regex: '.__manage') .listen(expectAsync1((notification) async { logger.finer('got enroll notification'); + final notificationKey = notification.key; + enrollmentId = notificationKey.substring( + 0, notificationKey.indexOf('.new.enrollments')); expect(notification.value, isNotNull); var notificationValueJson = jsonDecode(notification.value!); expect(notificationValueJson['encryptedApkamSymmetricKey'], @@ -258,7 +262,7 @@ void main() { AtOnboardingServiceImpl? onboardingService_2 = AtOnboardingServiceImpl(atSign, preference_1); - Future expectLaterFuture = expectLater( + expectLater( onboardingService_2.enroll( 'buzz', 'iphone', @@ -267,8 +271,8 @@ void main() { retryInterval: Duration(seconds: 5), ), throwsA(predicate((dynamic e) => - e is AtEnrollmentException && e.message == 'enrollment denied'))); - print(await expectLaterFuture); + e is AtEnrollmentException && + e.message == 'The enrollment: $enrollmentId is denied'))); await completer.future; await onboardingService_1.close();