Skip to content

Commit

Permalink
Merge pull request #1473 from atsign-foundation/auto_expire_enroll_key
Browse files Browse the repository at this point in the history
feat: Add expiry for enrollment requests
  • Loading branch information
sitaram-kalluri authored Sep 8, 2023
2 parents a1eea18 + 871be4a commit a5dcd21
Show file tree
Hide file tree
Showing 5 changed files with 468 additions and 37 deletions.
6 changes: 6 additions & 0 deletions packages/at_secondary_server/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,9 @@ sync:
#IMPORTANT NOTE : please set testingMode to true only if you know what you're doing. Set to false when not testing
testing:
testingMode: false

# APKAM enrollment configurations
enrollment:
# The maximum time in hours for an enrollment to expire, beyond which any action on enrollment is forbidden.
# Default values is 48 hours.
expiryInHours: 48
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,22 @@ class AtSecondaryConfig {

//Notification
static const bool _autoNotify = true;

// The maximum number of retries for a notification.
static const int _maxNotificationRetries = 30;

// The quarantine duration of an atsign. Notifications will be retried max_retries times, every quarantineDuration seconds approximately.
static const int _notificationQuarantineDuration = 10;

// The notifications queue will be processed every jobFrequency seconds. However, the notifications queue will always be processed
// *immediately* when a new notification is queued. When that happens, the queue processing will not run again until jobFrequency
// seconds have passed since the last queue-processing run completed.
static const int _notificationJobFrequency = 11;

// The time interval(in seconds) to notify latest commitID to monitor connections
// To disable to the feature, set to -1.
static const int _statsNotificationJobTimeInterval = 15;

// defines the time after which a notification expires in units of minutes. Notifications expire after 1440 minutes or 24 hours by default.
static const int _notificationExpiresAfterMins = 1440;

Expand Down Expand Up @@ -116,10 +121,14 @@ class AtSecondaryConfig {
? ConfigUtil.getPubspecConfig()!['version']
: null;

static final int _enrollmentExpiryInHours = 48;

static final Map<String, String> _envVars = Platform.environment;

static String? get secondaryServerVersion => _secondaryServerVersion;

static int get enrollmentExpiryInHours => _enrollmentExpiryInHours;

// TODO: Medium priority: Most (all?) getters in this class return a default value but the signatures currently
// allow for nulls. Should fix this as has been done for logLevel
// TODO: Low priority: Lots of very similar boilerplate code here. Not necessarily bad in this particular case, but
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ 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/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';
import 'package:meta/meta.dart';
import 'package:uuid/uuid.dart';
import 'abstract_verb_handler.dart';
import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart';
Expand All @@ -26,6 +29,10 @@ class EnrollVerbHandler extends AbstractVerbHandler {
@override
Verb getVerb() => enrollVerb;

@visibleForTesting
int enrollmentExpiryInMills =
Duration(hours: AtSecondaryConfig.enrollmentExpiryInHours).inMilliseconds;

@override
Future<void> processVerb(
Response response,
Expand Down Expand Up @@ -119,7 +126,9 @@ class EnrollVerbHandler extends AbstractVerbHandler {
enrollParams.appName!,
enrollParams.deviceName!,
enrollParams.apkamPublicKey!);

enrollmentValue.namespaces = enrollNamespaces;
enrollmentValue.requestType = EnrollRequestType.newEnrollment;
AtData enrollData;
if (atConnection.getMetaData().authType != null &&
atConnection.getMetaData().authType == AuthType.cram) {
// auto approve request from connection that is CRAM authenticated.
Expand All @@ -137,15 +146,19 @@ class EnrollVerbHandler extends AbstractVerbHandler {
// The keys with AT_PKAM_PUBLIC_KEY does not sync to client.
await keyStore.put(
AT_PKAM_PUBLIC_KEY, AtData()..data = enrollParams.apkamPublicKey!);
enrollData = AtData()..data = jsonEncode(enrollmentValue.toJson());
} else {
enrollmentValue.approval = EnrollApproval(EnrollStatus.pending.name);
await _storeNotification(key, enrollParams, currentAtSign);
responseJson['status'] = 'pending';
enrollData = AtData()
..data = jsonEncode(enrollmentValue.toJson())
// Set TTL to the pending enrollments.
// The enrollments will expire after configured
// expiry limit, beyond which any action (approve/deny/revoke) on an
// enrollment is forbidden
..metaData = (AtMetaData()..ttl = enrollmentExpiryInMills);
}

enrollmentValue.namespaces = enrollNamespaces;
enrollmentValue.requestType = EnrollRequestType.newEnrollment;
AtData enrollData = AtData()..data = jsonEncode(enrollmentValue.toJson());
logger.finer('enrollData: $enrollData');
await keyStore.put('$key$currentAtSign', enrollData, skipCommit: true);
}
Expand All @@ -160,39 +173,51 @@ class EnrollVerbHandler extends AbstractVerbHandler {
String? operation,
Map<dynamic, dynamic> responseJson) async {
final enrollmentIdFromParams = enrollParams.enrollmentId;
var key =
String enrollmentKey =
'$enrollmentIdFromParams.$newEnrollmentKeyPattern.$enrollManageNamespace';
logger.finer('key: $key$currentAtSign');
var enrollData;
try {
enrollData = await keyStore.get('$key$currentAtSign');
} on KeyNotFoundException {
throw AtEnrollmentException(
'enrollment id: $enrollmentIdFromParams not found in keystore');
}
if (enrollData != null) {
final existingAtData = enrollData.data;
var enrollDataStoreValue =
EnrollDataStoreValue.fromJson(jsonDecode(existingAtData));
logger.finer(
'Enrollment key: $enrollmentKey$currentAtSign | Enrollment operation: $operation');
// 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!));

enrollDataStoreValue.approval!.state =
_getEnrollStatusEnum(operation).name;
responseJson['status'] = _getEnrollStatusEnum(operation).name;
AtData updatedEnrollData = AtData()
..data = jsonEncode(enrollDataStoreValue.toJson());
await keyStore.put('$key$currentAtSign', updatedEnrollData,
skipCommit: true);
// 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 atData = AtData()..data = jsonEncode(valueJson);
await keyStore.put(apkamPublicKeyInKeyStore, atData);
await _storeEncryptionKeys(
enrollmentIdFromParams!, enrollParams, currentAtSign);
}
// 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;
responseJson['status'] = _getEnrollStatusEnum(operation).name;

// If an enrollment is approved, we need the enrollment to be active
// to subsequently revoke the enrollment. Hence reset TTL and
// expiredAt on metadata.
/* TODO: Currently TTL is reset on all the enrollments.
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);
// 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 atData = AtData()..data = jsonEncode(valueJson);
await keyStore.put(apkamPublicKeyInKeyStore, atData);
await _storeEncryptionKeys(
enrollmentIdFromParams!, enrollParams, currentAtSign);
}
responseJson['enrollmentId'] = enrollmentIdFromParams;
}
Expand Down Expand Up @@ -311,4 +336,40 @@ class EnrollVerbHandler extends AbstractVerbHandler {
'Error while storing notification key $enrollmentId. Error $e. Trace $trace');
}
}

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) {
throw AtEnrollmentException(
'Cannot approve a ${enrollDataStoreValue.approval!.state} enrollment. Only pending enrollments can be approved');
}
if (operation == 'revoke' &&
enrollDataStoreValue.approval!.state != EnrollStatus.approved.name) {
throw AtEnrollmentException(
'Cannot revoke a ${enrollDataStoreValue.approval!.state} enrollment. Only approved enrollments can be revoked');
}
}
}
Loading

0 comments on commit a5dcd21

Please sign in to comment.