diff --git a/packages/at_client/CHANGELOG.md b/packages/at_client/CHANGELOG.md index ee3fa385c..46422ed10 100644 --- a/packages/at_client/CHANGELOG.md +++ b/packages/at_client/CHANGELOG.md @@ -10,6 +10,7 @@ - at_chops to v1.0.7 - at_persistence_secondary_server to v3.0.60 - feat: Replace encryption methods from EncryptionUtils with AtChops method +- feat: Introduce feature to fetch all enrollment requests from the server ## 3.0.72 - chore: Minor change to allow us to support dart versions both before and after 3.2.0 specifically for this diff --git a/packages/at_client/lib/at_client.dart b/packages/at_client/lib/at_client.dart index 8ddc869de..598abef56 100644 --- a/packages/at_client/lib/at_client.dart +++ b/packages/at_client/lib/at_client.dart @@ -12,6 +12,8 @@ export 'package:at_client/src/preference/at_client_preference.dart'; export 'package:at_client/src/response/at_notification.dart'; export 'package:at_client/src/util/at_client_util.dart'; export 'package:at_client/src/util/encryption_util.dart'; +export 'package:at_client/src/util/enrollment_request.dart'; +export 'package:at_client/src/util/enroll_list_request_param.dart'; export 'package:at_client/src/service/notification_service.dart'; export 'package:at_client/src/service/sync_service.dart'; export 'package:at_client/src/service/sync/sync_result.dart'; diff --git a/packages/at_client/lib/src/client/at_client_impl.dart b/packages/at_client/lib/src/client/at_client_impl.dart index 08f023209..487d3fa42 100644 --- a/packages/at_client/lib/src/client/at_client_impl.dart +++ b/packages/at_client/lib/src/client/at_client_impl.dart @@ -649,6 +649,36 @@ class AtClientImpl implements AtClient, AtSignChangeListener { return AtChopsImpl(atChopsKeys); } + @override + Future> fetchEnrollmentRequests( + EnrollListRequestParam enrollmentListRequestParams) async { + // enrollmentListRequestParams for now is not used + // A server side enhancement request is created. https://github.com/atsign-foundation/at_server/issues/1748 + // On implementation of this enhancement/feature, the enrollListRequestParam object can be made use of + EnrollVerbBuilder enrollBuilder = EnrollVerbBuilder() + ..operation = EnrollOperationEnum.list + ..appName = enrollmentListRequestParams.appName + ..deviceName = enrollmentListRequestParams.deviceName; + + var response = await getRemoteSecondary() + ?.executeCommand(enrollBuilder.buildCommand(), auth: true); + + return _formatEnrollListResponse(response); + } + + List _formatEnrollListResponse(response) { + response = response?.replaceFirst('data:', ''); + Map enrollRequests = jsonDecode(response!); + List enrollRequestsFormatted = []; + EnrollmentRequest? enrollment; + for (var request in enrollRequests.entries) { + enrollment = EnrollmentRequest.fromJson(request.value); + enrollment.enrollmentKey = request.key; + enrollRequestsFormatted.add(enrollment); + } + return enrollRequestsFormatted; + } + @override Future stream(String sharedWith, String filePath, {String? namespace}) async { diff --git a/packages/at_client/lib/src/client/at_client_spec.dart b/packages/at_client/lib/src/client/at_client_spec.dart index d04ec9424..f21646dec 100644 --- a/packages/at_client/lib/src/client/at_client_spec.dart +++ b/packages/at_client/lib/src/client/at_client_spec.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:at_client/at_client.dart'; import 'package:at_client/src/client/local_secondary.dart'; import 'package:at_client/src/client/remote_secondary.dart'; import 'package:at_client/src/manager/sync_manager.dart'; @@ -572,4 +573,18 @@ abstract class AtClient { String? getCurrentAtSign(); EncryptionService? get encryptionService; + + /// Fetches all enrollment requests from the corresponding atServer; Formats the requests into a + /// List<[EnrollmentResponse]> + /// + /// Responses can be filtered using params provided through [EnrollListRequestParam] + /// ``` + /// e.g. + /// List enrollmentRequests = fetchEnrollmentRequests(EnrollRequestParams()); + /// enrollmentRequests now contains all the enrollment requests fetched from the server in the for of + /// EnrollmentRequest objects + /// ``` + @experimental + Future> fetchEnrollmentRequests( + EnrollListRequestParam enrollmentListRequest); } diff --git a/packages/at_client/lib/src/client/remote_secondary.dart b/packages/at_client/lib/src/client/remote_secondary.dart index 4110f2675..56b768c0c 100644 --- a/packages/at_client/lib/src/client/remote_secondary.dart +++ b/packages/at_client/lib/src/client/remote_secondary.dart @@ -33,10 +33,10 @@ class RemoteSecondary implements Secondary { logger = AtSignLogger('RemoteSecondary ($_atSign)'); _preference = preference; privateKey ??= preference.privateKey; - SecureSocketConfig secureSocketConfig = SecureSocketConfig(); - secureSocketConfig.decryptPackets = preference.decryptPackets; - secureSocketConfig.pathToCerts = preference.pathToCerts; - secureSocketConfig.tlsKeysSavePath = preference.tlsKeysSavePath; + SecureSocketConfig secureSocketConfig = SecureSocketConfig() + ..decryptPackets = preference.decryptPackets + ..pathToCerts = preference.pathToCerts + ..tlsKeysSavePath = preference.tlsKeysSavePath; atLookUp = AtLookupImpl(atSign, preference.rootDomain, preference.rootPort, privateKey: privateKey, cramSecret: preference.cramSecret, @@ -107,14 +107,8 @@ class RemoteSecondary implements Secondary { // ignore: prefer_typing_uninitialized_variables var verbResult; try { - logger.finer(logger.getLogMessageWithClientParticulars( - _preference.atClientParticulars, - 'Command sent to server: ${builder.buildCommand()}')); verbResult = await executeVerb(builder); verbResult = verbResult.replaceFirst('data:', ''); - logger.finer(logger.getLogMessageWithClientParticulars( - _preference.atClientParticulars, - 'Response from server: $verbResult')); } on AtException catch (e) { throw e ..stack(AtChainedException(Intent.fetchData, diff --git a/packages/at_client/lib/src/util/enroll_list_request_param.dart b/packages/at_client/lib/src/util/enroll_list_request_param.dart new file mode 100644 index 000000000..b41f94617 --- /dev/null +++ b/packages/at_client/lib/src/util/enroll_list_request_param.dart @@ -0,0 +1,6 @@ +/// class to store request parameters while fetching a list of enrollments +class EnrollListRequestParam { + String? appName; + String? deviceName; + String? namespace; +} diff --git a/packages/at_client/lib/src/util/enrollment_request.dart b/packages/at_client/lib/src/util/enrollment_request.dart new file mode 100644 index 000000000..6e25f561c --- /dev/null +++ b/packages/at_client/lib/src/util/enrollment_request.dart @@ -0,0 +1,40 @@ +import 'dart:collection'; +import 'dart:convert'; + +class EnrollmentRequest { + late String enrollmentKey; + late String appName; + late String deviceName; + late Map namespace; + + String get enrollmentId { + return extractEnrollmentId(enrollmentKey); + } + + @override + String toString() { + return 'Enrollment Request: enrollmentKey: $enrollmentKey | appName: $appName | deviceName: $deviceName | namespace: ${namespace.toString()}'; + } + + Map toJson() { + Map jsonMap = HashMap(); + jsonMap['enrollmentKey'] = enrollmentKey; + jsonMap['appName'] = appName; + jsonMap['deviceName'] = deviceName; + jsonMap['namespace'] = jsonEncode(namespace); + + return jsonMap; + } + + static EnrollmentRequest fromJson(Map json) { + EnrollmentRequest enrollmentRequest = EnrollmentRequest(); + return enrollmentRequest + ..appName = json['appName'] + ..deviceName = json['deviceName'] + ..namespace = json['namespace']; + } + + static String extractEnrollmentId(String enrollmentKey) { + return enrollmentKey.split('.')[0]; + } +} diff --git a/packages/at_client/test/at_client_impl_test.dart b/packages/at_client/test/at_client_impl_test.dart index 39f9a16a6..750a163b7 100644 --- a/packages/at_client/test/at_client_impl_test.dart +++ b/packages/at_client/test/at_client_impl_test.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:at_client/at_client.dart'; import 'package:at_client/src/compaction/at_commit_log_compaction.dart'; import 'package:at_client/src/service/notification_service_impl.dart'; @@ -20,6 +22,8 @@ class MockAtCompactionJob extends Mock implements AtCompactionJob { } } +class MockRemoteSecondary extends Mock implements RemoteSecondary {} + void main() { group('A group of at client impl create tests', () { final String atSign = '@alice'; @@ -251,7 +255,7 @@ void main() { }); }); - group('A group of tests related to setting enrollmentId', () { + group('A group of tests related to apkam/enrollments', () { test( 'A test to verify enrollmentId is set in atClient after calling setCurrentAtSign', () async { @@ -261,5 +265,63 @@ void main() { enrollmentId: testEnrollmentId); expect(atClientManager.atClient.enrollmentId, testEnrollmentId); }); + + MockRemoteSecondary mockRemoteSecondary = MockRemoteSecondary(); + + test('verify behaviour of fetchEnrollmentRequests()', () async { + String currentAtsign = '@apkam'; + String enrollKey1 = + '0acdeb4d-1a2e-43e4-93bd-378f1d366ea7.new.enrollments.__manage$currentAtsign'; + String enrollValue1 = + '{"appName":"buzz","deviceName":"pixel","namespace":{"buzz":"rw"}}'; + String enrollKey2 = + '9beefa26-3384-4f10-81a6-0deaa4332669.new.enrollments.__manage$currentAtsign'; + String enrollValue2 = + '{"appName":"buzz","deviceName":"pixel","namespace":{"buzz":"rw"}}'; + String enrollKey3 = + 'a6bbef17-c7bf-46f4-a172-1ed7b3b443bc.new.enrollments.__manage$currentAtsign'; + String enrollValue3 = + '{"appName":"buzz","deviceName":"pixel","namespace":{"buzz":"rw"}}'; + when(() => + mockRemoteSecondary.executeCommand('enroll:list\n', + auth: true)).thenAnswer((_) => Future.value('data:{"$enrollKey1":' + '$enrollValue1,"$enrollKey2":$enrollValue2,"$enrollKey3":$enrollValue3}')); + + AtClient? client = await AtClientImpl.create( + currentAtsign, 'buzz', AtClientPreference(), + remoteSecondary: mockRemoteSecondary); + AtClientImpl? clientImpl = client as AtClientImpl; + + List requests = + await clientImpl.fetchEnrollmentRequests(EnrollListRequestParam()); + expect(requests.length, 3); + expect(requests[0].enrollmentKey, enrollKey1); + expect(requests[0].appName, jsonDecode(enrollValue1)['appName']); + expect(requests[0].deviceName, jsonDecode(enrollValue1)['deviceName']); + expect(requests[0].namespace, jsonDecode(enrollValue1)['namespace']); + // the following statement asserts that the enrollment.enrollmentId getter fetches the correct enrollment id + expect(requests[0].enrollmentKey.contains(enrollKey1), true); + + expect(requests[1].enrollmentKey, enrollKey2); + expect(requests[1].appName, jsonDecode(enrollValue2)['appName']); + expect(requests[1].deviceName, jsonDecode(enrollValue2)['deviceName']); + expect(requests[1].namespace, jsonDecode(enrollValue2)['namespace']); + expect(requests[1].enrollmentKey.contains(enrollKey2), true); + + expect(requests[2].enrollmentKey, enrollKey3); + expect(requests[2].appName, jsonDecode(enrollValue3)['appName']); + expect(requests[2].deviceName, jsonDecode(enrollValue3)['deviceName']); + expect(requests[2].namespace, jsonDecode(enrollValue3)['namespace']); + expect(requests[2].enrollmentKey.contains(enrollKey3), true); + }); + + test('validate EnrollRequest.extractEnrollmentId()', () { + String enrollmentKey = + '0acdeb4d-1a2e-43e4-93bd-378f1d366ea7.new.enrollments.__manage@random'; + String enrollmentId = '0acdeb4d-1a2e-43e4-93bd-378f1d366ea7'; + + expect( + EnrollmentRequest.extractEnrollmentId(enrollmentKey), enrollmentId); + }); }); } diff --git a/tests/at_functional_test/test/enrollment_test.dart b/tests/at_functional_test/test/enrollment_test.dart index 806f84a41..3f4017c0b 100644 --- a/tests/at_functional_test/test/enrollment_test.dart +++ b/tests/at_functional_test/test/enrollment_test.dart @@ -26,7 +26,7 @@ void main() { await setLastReceivedNotificationDateTime(); }); - void _stopSubscriptions() { + void stopSubscriptions() { atClientManager.atClient.notificationService.stopAllSubscriptions(); print('subscriptions stopped'); } @@ -87,9 +87,75 @@ void main() { print('got enrollment notification: $enrollNotification'); expect(enrollNotification.key, '$enrollmentIdFromServer.new.enrollments.__manage'); - _stopSubscriptions(); + stopSubscriptions(); }, count: 1, max: 1)); }); + + test( + 'validate client functionality to fetch pending enrollments on legacy pkam authenticated client', + () async { + atClientManager = await TestUtils.initAtClient(atSign, 'new_app'); + AtClient? client = atClientManager.atClient; + // fetch first otp + String? otp = + await TestUtils.executeCommandAndParse(client, 'otp:get', auth: true); + expect(otp, isNotNull); + // create first enrollment request + RemoteSecondary? secondRemoteSecondary = + RemoteSecondary(atSign, getClient2Preferences()); + var apkamPublicKey = + at_demos.pkamPublicKeyMap['@eve🛠']; // can be any random public key + var newEnrollRequest = TestUtils.formatCommand( + 'enroll:request:{"appName":"new_app","deviceName":"pixel","namespaces":{"new_app":"rw"},"otp":"$otp","apkamPublicKey":"$apkamPublicKey"}'); + var enrollResponse = await TestUtils.executeCommandAndParse( + null, newEnrollRequest, + remoteSecondary: secondRemoteSecondary); + Map enrollResponse1JsonDecoded = + jsonDecode(enrollResponse!); + expect(enrollResponse1JsonDecoded['enrollmentId'], isNotNull); + expect(enrollResponse1JsonDecoded['status'], 'pending'); + + // fetch second otp + otp = await TestUtils.executeCommandAndParse(client, 'otp:get', auth: true); + expect(otp, isNotNull); + // create second enrollment request + newEnrollRequest = TestUtils.formatCommand( + 'enroll:request:{"appName":"new_app","deviceName":"pixel7","namespaces":{"new_app":"rw", "wavi":"r"},"otp":"$otp","apkamPublicKey":"$apkamPublicKey"}'); + enrollResponse = await TestUtils.executeCommandAndParse( + null, newEnrollRequest, + remoteSecondary: secondRemoteSecondary); + var enrollResponse2JsonDecoded = jsonDecode(enrollResponse!); + expect(enrollResponse2JsonDecoded['enrollmentId'], isNotNull); + expect(enrollResponse2JsonDecoded['status'], 'pending'); + + // fetch enrollment requests through client + List enrollmentRequests = + await client.fetchEnrollmentRequests(EnrollListRequestParam()); + + expect(enrollmentRequests.length, 4); + // 4 entries - 2 entries from this test + // + 2 entries from the other test in this file. + + String firstEnrollmentKey = + getEnrollmentKey(enrollResponse1JsonDecoded['enrollmentId'], atSign); + String secondEnrollmentKey = + getEnrollmentKey(enrollResponse2JsonDecoded['enrollmentId'], atSign); + int matchCount = 0; + for (var request in enrollmentRequests) { + if (request.enrollmentKey == firstEnrollmentKey) { + expect(request.namespace['new_app'], 'rw'); + expect(request.deviceName, 'pixel'); + matchCount++; + } else if (request.enrollmentKey == secondEnrollmentKey) { + expect(request.namespace['new_app'], 'rw'); + expect(request.namespace['wavi'], 'r'); + expect(request.deviceName, 'pixel7'); + matchCount++; + } + } + // this counter is to assert that the list of requests has exactly two request matches + expect(matchCount, 2); + }); } Future setLastReceivedNotificationDateTime() async { @@ -115,6 +181,10 @@ Future setLastReceivedNotificationDateTime() async { .put(lastReceivedNotificationAtKey, jsonEncode(atNotification.toJson())); } +String getEnrollmentKey(String enrollmentId, String atsign) { + return '$enrollmentId.new.enrollments.__manage$atsign'; +} + AtClientPreference getClient2Preferences() { return AtClientPreference() ..commitLogPath = 'test/hive/client_2/commit' diff --git a/tests/at_functional_test/test/test_utils.dart b/tests/at_functional_test/test/test_utils.dart index 6984b1128..cf7b31d0a 100644 --- a/tests/at_functional_test/test/test_utils.dart +++ b/tests/at_functional_test/test/test_utils.dart @@ -8,6 +8,7 @@ import 'package:at_client/at_client.dart'; import 'package:at_functional_test/src/at_demo_credentials.dart' as demo_credentials; + class TestUtils { static AtClientPreference getPreference(String atsign) { var preference = AtClientPreference(); @@ -52,4 +53,16 @@ class TestUtils { atClientManager.atClient, currentAtSign); return atClientManager; } + + static String formatCommand(String command){ + if(!command.contains('\n')) return '$command\n'; + return command; + } + + static Future executeCommandAndParse(AtClient? client, command, {bool auth = false, RemoteSecondary? remoteSecondary}) async { + remoteSecondary ??= client?.getRemoteSecondary(); + String? response = await remoteSecondary?.executeCommand(formatCommand(command), auth: auth); + print('Command: $command -> Response: $response'); + return response?.replaceFirst('data:', ''); + } }