Skip to content

Commit

Permalink
Merge pull request #1536 from atsign-foundation/nwe_expire_logic
Browse files Browse the repository at this point in the history
fix: new expiry logic and error codes for enrollments
  • Loading branch information
murali-shris authored Sep 12, 2023
2 parents 97b378c + da82205 commit 68517d3
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 134 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:at_secondary/src/verb/manager/response_handler_manager.dart';
import 'package:at_server_spec/at_server_spec.dart';
import 'package:at_server_spec/at_verb_spec.dart';
import 'package:at_utils/at_logger.dart';
import 'package:at_secondary/src/utils/secondary_util.dart';

final String paramFullCommandAsReceived = 'FullCommandAsReceived';

Expand Down Expand Up @@ -89,6 +90,10 @@ abstract class AbstractVerbHandler implements VerbHandler {
AtData enrollData = await keyStore.get(enrollmentKey);
EnrollDataStoreValue enrollDataStoreValue =
EnrollDataStoreValue.fromJson(jsonDecode(enrollData.data!));
if (!SecondaryUtil.isActiveKey(enrollData) &&
enrollDataStoreValue.approval!.state != EnrollStatus.approved.name) {
enrollDataStoreValue.approval?.state = EnrollStatus.expired.name;
}
return enrollDataStoreValue;
} on KeyNotFoundException {
logger.severe('$enrollmentKey does not exist in the keystore');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import 'dart:collection';
import 'dart:convert';
import 'package:at_commons/at_commons.dart';
import 'package:at_secondary/src/connection/inbound/inbound_connection_metadata.dart';
import 'package:at_secondary/src/server/at_secondary_config.dart';
import 'package:at_secondary/src/server/at_secondary_impl.dart';
import 'package:at_secondary/src/server/at_secondary_config.dart';
import 'package:at_secondary/src/constants/enroll_constants.dart';
import 'package:at_secondary/src/enroll/enroll_datastore_value.dart';
import 'package:at_secondary/src/utils/notification_util.dart';
import 'package:at_secondary/src/utils/secondary_util.dart';
import 'package:at_secondary/src/verb/handler/otp_verb_handler.dart';
import 'package:at_server_spec/at_server_spec.dart';
import 'package:at_server_spec/at_verb_spec.dart';
Expand Down Expand Up @@ -47,23 +46,31 @@ class EnrollVerbHandler extends AbstractVerbHandler {
throw UnAuthenticatedException(
'Cannot $operation enrollment without authentication');
}
EnrollParams? enrollVerbParams;
try {
var enrollVerbParams;
if (verbParams[enrollParams] != null) {
// Ensure that enrollParams are present for all enroll operation
// Exclude operation 'list' which does not have enrollParams
if (verbParams[enrollParams] == null) {
if (operation != 'list') {
logger.severe(
'Enroll params is empty | EnrollParams: ${verbParams[enrollParams]}');
throw IllegalArgumentException('Enroll parameters not provided');
}
} else {
enrollVerbParams =
EnrollParams.fromJson(jsonDecode(verbParams[enrollParams]!));
}
switch (operation) {
case 'request':
await _handleEnrollmentRequest(
enrollVerbParams, currentAtSign, responseJson, atConnection);
enrollVerbParams!, currentAtSign, responseJson, atConnection);
break;

case 'approve':
case 'deny':
case 'revoke':
await _handleEnrollmentPermissions(
enrollVerbParams, currentAtSign, operation, responseJson);
await _handleEnrollmentPermissions(enrollVerbParams!, currentAtSign,
operation, responseJson, response);
break;

case 'list':
Expand All @@ -72,14 +79,11 @@ class EnrollVerbHandler extends AbstractVerbHandler {
return;
}
} catch (e, stackTrace) {
response.isError = true;
response.errorMessage = e.toString();
responseJson['status'] = 'exception';
responseJson['reason'] = e.toString();
logger.severe('Exception: $e\n$stackTrace');
rethrow;
}
response.data = jsonEncode(responseJson);
return;
}

/// Enrollment requests details are persisted in the keystore and are excluded from
Expand Down Expand Up @@ -171,26 +175,44 @@ class EnrollVerbHandler extends AbstractVerbHandler {
EnrollParams enrollParams,
currentAtSign,
String? operation,
Map<dynamic, dynamic> responseJson) async {
Map<dynamic, dynamic> responseJson,
Response response) async {
final enrollmentIdFromParams = enrollParams.enrollmentId;
String enrollmentKey =
'$enrollmentIdFromParams.$newEnrollmentKeyPattern.$enrollManageNamespace';
logger.finer(
'Enrollment key: $enrollmentKey$currentAtSign | Enrollment operation: $operation');
EnrollDataStoreValue? enrollDataStoreValue;
EnrollStatus? enrollStatus;
// Fetch and returns enrollment data from the keystore.
// Throw AtEnrollmentException, IF
// 1. Enrollment key is not present in keystore
// 2. Enrollment key is not active
AtData enrollData = await _fetchEnrollmentDataFromKeyStore(
enrollmentKey, currentAtSign, enrollmentIdFromParams);
var enrollDataStoreValue =
EnrollDataStoreValue.fromJson(jsonDecode(enrollData.data!));

try {
enrollDataStoreValue =
await getEnrollDataStoreValue('$enrollmentKey$currentAtSign');
} on KeyNotFoundException {
// When an enrollment key is expired or invalid
enrollStatus = EnrollStatus.expired;
}
enrollStatus ??=
getEnrollStatusFromString(enrollDataStoreValue!.approval!.state);
// Validates if enrollment is not expired
if (EnrollStatus.expired == enrollStatus) {
response.isError = true;
response.errorCode = 'AT0028';
response.errorMessage =
'enrollment_id: $enrollmentIdFromParams is expired or invalid';
}
if (response.isError) {
return;
}
// Verifies whether the enrollment state matches the intended state
// Throws AtEnrollmentException, if the enrollment state is different from
// the intended state
_verifyEnrollmentStateBeforeAction(operation, enrollDataStoreValue);
enrollDataStoreValue.approval!.state = _getEnrollStatusEnum(operation).name;
_verifyEnrollmentStateBeforeAction(operation, enrollStatus);
enrollDataStoreValue!.approval!.state =
_getEnrollStatusEnum(operation).name;
responseJson['status'] = _getEnrollStatusEnum(operation).name;

// If an enrollment is approved, we need the enrollment to be active
Expand All @@ -200,20 +222,13 @@ class EnrollVerbHandler extends AbstractVerbHandler {
However, if the enrollment state is denied or revoked,
unless we wanted to display denied or revoked enrollments in the UI,
we can let the TTL be, so that the enrollment will be deleted subsequently.*/
await keyStore.put(
'$enrollmentKey$currentAtSign',
AtData()
..data = jsonEncode(enrollDataStoreValue.toJson())
..metaData = (enrollData.metaData
?..ttl = 0
..expiresAt = null),
skipCommit: true);
await _updateEnrollmentValueAndResetTTL(
'$enrollmentKey$currentAtSign', enrollDataStoreValue);
// when enrollment is approved store the apkamPublicKey of the enrollment
if (operation == 'approve') {
var apkamPublicKeyInKeyStore =
'public:${enrollDataStoreValue.appName}.${enrollDataStoreValue.deviceName}.pkam.$pkamNamespace.__public_keys$currentAtSign';
var valueJson = {};
valueJson[apkamPublicKey] = enrollDataStoreValue.apkamPublicKey;
var valueJson = {'apkamPublicKey': enrollDataStoreValue.apkamPublicKey};
var atData = AtData()..data = jsonEncode(valueJson);
await keyStore.put(apkamPublicKeyInKeyStore, atData);
await _storeEncryptionKeys(
Expand Down Expand Up @@ -280,11 +295,13 @@ class EnrollVerbHandler extends AbstractVerbHandler {
if (_doesEnrollmentHaveManageNamespace(enrollDataStoreValue)) {
await _fetchAllEnrollments(enrollmentKeysList, enrollmentRequestsMap);
} else {
enrollmentRequestsMap[enrollmentKey] = {
'appName': enrollDataStoreValue.appName,
'deviceName': enrollDataStoreValue.deviceName,
'namespace': enrollDataStoreValue.namespaces
};
if (enrollDataStoreValue.approval!.state != EnrollStatus.expired.name) {
enrollmentRequestsMap[enrollmentKey] = {
'appName': enrollDataStoreValue.appName,
'deviceName': enrollDataStoreValue.deviceName,
'namespace': enrollDataStoreValue.namespaces
};
}
}
return jsonEncode(enrollmentRequestsMap);
}
Expand All @@ -294,11 +311,13 @@ class EnrollVerbHandler extends AbstractVerbHandler {
for (var enrollmentKey in enrollmentKeysList) {
EnrollDataStoreValue enrollDataStoreValue =
await getEnrollDataStoreValue(enrollmentKey);
enrollmentRequestsMap[enrollmentKey] = {
'appName': enrollDataStoreValue.appName,
'deviceName': enrollDataStoreValue.deviceName,
'namespace': enrollDataStoreValue.namespaces
};
if (enrollDataStoreValue.approval!.state != EnrollStatus.expired.name) {
enrollmentRequestsMap[enrollmentKey] = {
'appName': enrollDataStoreValue.appName,
'deviceName': enrollDataStoreValue.deviceName,
'namespace': enrollDataStoreValue.namespaces
};
}
}
}

Expand Down Expand Up @@ -337,39 +356,34 @@ class EnrollVerbHandler extends AbstractVerbHandler {
}
}

Future<AtData> _fetchEnrollmentDataFromKeyStore(
String enrollmentKey, currentAtSign, String? enrollmentId) async {
AtData enrollData;
// KeyStore.get will not return null. If the value is null, keyStore.get
// throws KeyNotFoundException.
// So, enrollData will NOT be null.
try {
enrollData = await keyStore.get('$enrollmentKey$currentAtSign');
} on KeyNotFoundException {
throw AtEnrollmentException(
'enrollment id: $enrollmentId not found in keystore');
}
// If enrollment is not active, throw AtEnrollmentException
if (!SecondaryUtil.isActiveKey(enrollData)) {
throw AtEnrollmentException('The enrollment $enrollmentId is expired');
}
return enrollData;
}

/// Verifies whether the enrollment state matches the intended state.
/// Throws AtEnrollmentException: If the enrollment state is different
/// from the intended state.
void _verifyEnrollmentStateBeforeAction(
String? operation, EnrollDataStoreValue enrollDataStoreValue) {
if (operation == 'approve' &&
enrollDataStoreValue.approval!.state != EnrollStatus.pending.name) {
String? operation, EnrollStatus enrollStatus) {
if (operation == 'approve' && EnrollStatus.pending != enrollStatus) {
throw AtEnrollmentException(
'Cannot approve a ${enrollDataStoreValue.approval!.state} enrollment. Only pending enrollments can be approved');
'Cannot approve a ${enrollStatus.name} enrollment. Only pending enrollments can be approved');
}
if (operation == 'revoke' &&
enrollDataStoreValue.approval!.state != EnrollStatus.approved.name) {
if (operation == 'revoke' && EnrollStatus.approved != enrollStatus) {
throw AtEnrollmentException(
'Cannot revoke a ${enrollDataStoreValue.approval!.state} enrollment. Only approved enrollments can be revoked');
'Cannot revoke a ${enrollStatus.name} enrollment. Only approved enrollments can be revoked');
}
}

Future<void> _updateEnrollmentValueAndResetTTL(
String enrollmentKey, EnrollDataStoreValue enrollDataStoreValue) async {
// Fetch the existing data
AtMetaData? enrollMetaData = await keyStore.getMeta(enrollmentKey);
// Update key with new data
// only update ttl, expiresAt in metadata to preserve all the other valid data fields
enrollMetaData?.ttl = 0;
enrollMetaData?.expiresAt = null;
await keyStore.put(
enrollmentKey,
AtData()
..data = jsonEncode(enrollDataStoreValue.toJson())
..metaData = enrollMetaData,
skipCommit: true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,25 +86,25 @@ class PkamVerbHandler extends AbstractVerbHandler {
String enrollId, String atSign) async {
String enrollmentKey =
'$enrollId.$newEnrollmentKeyPattern.$enrollManageNamespace$atSign';
var enrollData = await keyStore.get(enrollmentKey);
final atData = enrollData.data;
final enrollDataStoreValue =
EnrollDataStoreValue.fromJson(jsonDecode(atData));
EnrollStatus enrollStatus =
EnrollStatus.values.byName(enrollDataStoreValue.approval!.state);

late final EnrollDataStoreValue enrollDataStoreValue;
ApkamVerificationResult apkamResult = ApkamVerificationResult();
apkamResult.response = _getApprovalStatus(
enrollStatus, enrollId, enrollDataStoreValue.approval!.state);
EnrollStatus? enrollStatus;
try {
enrollDataStoreValue = await getEnrollDataStoreValue(enrollmentKey);
enrollStatus = getEnrollStatusFromString(enrollDataStoreValue.approval!.state);
} on KeyNotFoundException catch (e) {
logger.finer('Caught exception trying to fetch enrollment key: $e');
enrollStatus = EnrollStatus.expired;
}
apkamResult.response = _getApprovalStatus(enrollStatus, enrollId);
if (apkamResult.response.isError) {
return apkamResult;
}
apkamResult.publicKey = enrollDataStoreValue.apkamPublicKey;
return apkamResult;
}

Response _getApprovalStatus(
EnrollStatus enrollStatus, enrollId, approvalState) {
Response _getApprovalStatus(EnrollStatus enrollStatus, enrollId) {
Response response = Response();
switch (enrollStatus) {
case EnrollStatus.denied:
Expand All @@ -125,6 +125,11 @@ class PkamVerbHandler extends AbstractVerbHandler {
response.errorCode = 'AT0027';
response.errorMessage = 'enrollment_id: $enrollId is revoked';
break;
case EnrollStatus.expired:
response.isError = true;
response.errorCode = 'AT0028';
response.errorMessage = 'enrollment_id: $enrollId is expired or invalid';
break;
default:
response.isError = true;
response.errorCode = 'AT0026';
Expand All @@ -143,7 +148,7 @@ class PkamVerbHandler extends AbstractVerbHandler {
bool isValidSignature = false;
var storedSecret = await keyStore.get('private:$sessionId$atSign');
storedSecret = storedSecret?.data;
if(signature == null || signature.isEmpty ){
if (signature == null || signature.isEmpty) {
logger.severe('inputSignature is null/empty');
return false;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/at_secondary_server/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ dependencies:
collection: 1.18.0
basic_utils: 5.6.1
ecdsa: 0.0.4
at_commons: 3.0.54
at_commons: 3.0.55
at_utils: 3.0.15
at_chops: 1.0.4
at_lookup: 3.0.40
Expand Down
Loading

0 comments on commit 68517d3

Please sign in to comment.