Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: add new method to fetch enrollment requests #1222

Merged
merged 8 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/at_client/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
## 3.0.75
- feat: Replace encryption methods from EncryptionUtils with AtChops method
srieteja marked this conversation as resolved.
Show resolved Hide resolved
## 3.0.74
- build[deps]: Upgraded dependencies for the following packages:
- at_chops to v2.0.0
Expand All @@ -9,7 +11,7 @@
- at_lookup to v3.0.44
- 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
Expand Down
2 changes: 2 additions & 0 deletions packages/at_client/lib/at_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
30 changes: 30 additions & 0 deletions packages/at_client/lib/src/client/at_client_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,36 @@ class AtClientImpl implements AtClient, AtSignChangeListener {
return AtChopsImpl(atChopsKeys);
}

@override
Future<List<EnrollmentRequest>> 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<EnrollmentRequest> _formatEnrollListResponse(response) {
response = response?.replaceFirst('data:', '');
Map<String, dynamic> enrollRequests = jsonDecode(response!);
List<EnrollmentRequest> 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<AtStreamResponse> stream(String sharedWith, String filePath,
{String? namespace}) async {
Expand Down
15 changes: 15 additions & 0 deletions packages/at_client/lib/src/client/at_client_spec.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<EnrollmentRequest> enrollmentRequests = fetchEnrollmentRequests(EnrollRequestParams());
/// enrollmentRequests now contains all the enrollment requests fetched from the server in the for of
/// EnrollmentRequest objects
/// ```
@experimental
Future<List<EnrollmentRequest>> fetchEnrollmentRequests(
EnrollListRequestParam enrollmentListRequest);
}
14 changes: 4 additions & 10 deletions packages/at_client/lib/src/client/remote_secondary.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class AtClientConfig {

/// Represents the at_client version.
/// Must always be the same as the actual version in pubspec.yaml
final String atClientVersion = '3.0.74';
final String atClientVersion = '3.0.75';

/// Represents the client commit log compaction time interval
///
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/// class to store request parameters while fetching a list of enrollments
class EnrollListRequestParam {
String? appName;
String? deviceName;
String? namespace;
}
40 changes: 40 additions & 0 deletions packages/at_client/lib/src/util/enrollment_request.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:collection';
import 'dart:convert';

class EnrollmentRequest {
late String enrollmentKey;
late String appName;
late String deviceName;
late Map<String, dynamic> namespace;

String get enrollmentId {
return extractEnrollmentId(enrollmentKey);
}

@override
String toString() {
return 'Enrollment Request: enrollmentKey: $enrollmentKey | appName: $appName | deviceName: $deviceName | namespace: ${namespace.toString()}';
}

Map<String, dynamic> toJson() {
Map<String, dynamic> jsonMap = HashMap();
jsonMap['enrollmentKey'] = enrollmentKey;
jsonMap['appName'] = appName;
jsonMap['deviceName'] = deviceName;
jsonMap['namespace'] = jsonEncode(namespace);

return jsonMap;
}

static EnrollmentRequest fromJson(Map<String, dynamic> json) {
EnrollmentRequest enrollmentRequest = EnrollmentRequest();
return enrollmentRequest
..appName = json['appName']
..deviceName = json['deviceName']
..namespace = json['namespace'];
}

static String extractEnrollmentId(String enrollmentKey) {
return enrollmentKey.split('.')[0];
}
}
4 changes: 2 additions & 2 deletions packages/at_client/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: The at_client library is the non-platform specific Client SDK which
##
##
## NB: When incrementing the version, please also increment the version in AtClientConfig file
version: 3.0.74
version: 3.0.75
## NB: When incrementing the version, please also increment the version in AtClientConfig file
##

Expand Down Expand Up @@ -46,4 +46,4 @@ dev_dependencies:
at_demo_data: ^1.0.1
coverage: ^1.5.0
mocktail: ^0.3.0
dart_code_metrics: ^4.17.0
dart_code_metrics: ^4.17.0
64 changes: 63 additions & 1 deletion packages/at_client/test/at_client_impl_test.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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 {
Expand All @@ -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<EnrollmentRequest> 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);
});
});
}
74 changes: 72 additions & 2 deletions tests/at_functional_test/test/enrollment_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ void main() {
await setLastReceivedNotificationDateTime();
});

void _stopSubscriptions() {
void stopSubscriptions() {
atClientManager.atClient.notificationService.stopAllSubscriptions();
print('subscriptions stopped');
}
Expand Down Expand Up @@ -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<String, dynamic> 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<EnrollmentRequest> 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<void> setLastReceivedNotificationDateTime() async {
Expand All @@ -115,6 +181,10 @@ Future<void> 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'
Expand Down
13 changes: 13 additions & 0 deletions tests/at_functional_test/test/test_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<String?> 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:', '');
}
}
Loading