Skip to content

Commit

Permalink
Merge pull request #2085 from atsign-foundation/2074-introducing-auto…
Browse files Browse the repository at this point in the history
…-expiry-and-time-to-birth-features-for-apkam-keys

fix: Introduce time duration for apkam keys to auto expire
  • Loading branch information
gkc authored Sep 25, 2024
2 parents 19da367 + 75b016c commit 997dfbc
Show file tree
Hide file tree
Showing 23 changed files with 613 additions and 171 deletions.
3 changes: 3 additions & 0 deletions packages/at_secondary_server/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
- fix: LatestCommitEntryOfEachKey metric fixed to return commit log entries till last commitID instead of default limit 25.
## 3.0.50
- fix: Enhance namespace authorisation check to verify when namespace has a period in it
- feat: Enable expiration of APKAM keys based on the specified duration.

## 3.0.49
- feat: Enforce superset access check for approving apps
- fix: respect isEncrypted:false if supplied in the notify: command, and
Expand All @@ -14,6 +16,7 @@
notifications from the server to the client.
- build[deps]: Upgraded the following package:
- at_persistence_secondary_server to v3.0.63

## 3.0.48
- feat Add expiresAt and availableAt params to notify:list response

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class EnrollDataStoreValue {
EnrollRequestType? requestType;
EnrollApproval? approval;
String? encryptedAPKAMSymmetricKey;
Duration apkamKeysExpiryDuration = Duration(milliseconds: 0);

EnrollDataStoreValue(
this.sessionId, this.appName, this.deviceName, this.apkamPublicKey);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import 'dart:convert';

import 'package:at_commons/at_commons.dart';
import 'package:at_persistence_secondary_server/at_persistence_secondary_server.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/server/at_secondary_impl.dart';
import 'package:at_secondary/src/utils/secondary_util.dart';
import 'package:at_utils/at_logger.dart';

/// Manages enrollment data in the secondary server.
///
/// This class provides methods to retrieve and store enrollment data
/// associated with a given enrollment ID. It interacts with the
/// SecondaryKeyStore to persist and retrieve enrollment information.
class EnrollmentManager {
final SecondaryKeyStore _keyStore;

final logger = AtSignLogger('AtSecondaryServer');

/// Creates an instance of [EnrollmentManager].
///
/// The [keyStore] is required to interact with the persistence layer.
EnrollmentManager(this._keyStore);

/// Retrieves the enrollment data for a given [enrollmentId].
///
/// This method constructs an enrollment key, fetches the corresponding
/// data from the key store, and returns it as an [EnrollDataStoreValue].
/// If the key is not found, a [KeyNotFoundException] is thrown.
///
/// If the retrieved enrollment data is no longer active, the status
/// will be set to `expired`.
///
/// Returns:
/// An [EnrollDataStoreValue] containing the enrollment details.
///
/// Throws:
/// [KeyNotFoundException] if the enrollment key does not exist.
Future<EnrollDataStoreValue> get(String enrollmentId) async {
String enrollmentKey = buildEnrollmentKey(enrollmentId);
try {
AtData enrollData = await _keyStore.get(enrollmentKey);
EnrollDataStoreValue enrollDataStoreValue =
EnrollDataStoreValue.fromJson(jsonDecode(enrollData.data!));

if (!SecondaryUtil.isActiveKey(enrollData)) {
enrollDataStoreValue.approval?.state = EnrollmentStatus.expired.name;
}

return enrollDataStoreValue;
} on KeyNotFoundException {
logger.severe('$enrollmentKey does not exist in the keystore');
rethrow;
}
}

/// Constructs the enrollment key based on the provided [enrollmentId].
///
/// The key format combines the [enrollmentId], a new enrollment key pattern,
/// and the current AtSign.
///
/// Returns:
/// A [String] representing the enrollment key.
String buildEnrollmentKey(String enrollmentId) {
return '$enrollmentId.$newEnrollmentKeyPattern.$enrollManageNamespace${AtSecondaryServerImpl.getInstance().currentAtSign}';
}

/// Stores the enrollment data associated with the given [enrollmentId].
///
/// This method constructs an enrollment key and saves the provided [AtData]
/// to the key store. The skipCommit is set to true, to prevent the enrollment
/// data being synced to the client(s).
///
/// Parameters:
/// - [enrollmentId]: The ID associated with the enrollment.
/// - [atData]: The [AtData] object to be stored.
Future<void> put(String enrollmentId, AtData atData) async {
String enrollmentKey = buildEnrollmentKey(enrollmentId);
await _keyStore.put(enrollmentKey, atData, skipCommit: true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import 'dart:math';
import 'package:at_commons/at_commons.dart';
import 'package:at_lookup/at_lookup.dart';
import 'package:at_persistence_secondary_server/at_persistence_secondary_server.dart';
import 'package:at_secondary/src/caching/cache_refresh_job.dart';
import 'package:at_secondary/src/caching/cache_manager.dart';
import 'package:at_secondary/src/caching/cache_refresh_job.dart';
import 'package:at_secondary/src/connection/inbound/inbound_connection_manager.dart';
import 'package:at_secondary/src/connection/outbound/outbound_client_manager.dart';
import 'package:at_secondary/src/connection/stream_manager.dart';
import 'package:at_secondary/src/enroll/enrollment_manager.dart';
import 'package:at_secondary/src/exception/global_exception_handler.dart';
import 'package:at_secondary/src/notification/notification_manager_impl.dart';
import 'package:at_secondary/src/notification/queue_manager.dart';
Expand All @@ -31,8 +32,8 @@ import 'package:at_server_spec/at_server_spec.dart';
import 'package:at_server_spec/at_verb_spec.dart';
import 'package:at_utils/at_utils.dart';
import 'package:crypton/crypton.dart';
import 'package:uuid/uuid.dart';
import 'package:meta/meta.dart';
import 'package:uuid/uuid.dart';

/// [AtSecondaryServerImpl] is a singleton class which implements [AtSecondaryServer]
class AtSecondaryServerImpl implements AtSecondaryServer {
Expand Down Expand Up @@ -106,6 +107,7 @@ class AtSecondaryServerImpl implements AtSecondaryServer {
late var atCommitLogCompactionConfig;
late var atAccessLogCompactionConfig;
late var atNotificationCompactionConfig;
late EnrollmentManager enrollmentManager;

@override
void setExecutor(VerbExecutor executor) {
Expand Down Expand Up @@ -169,6 +171,9 @@ class AtSecondaryServerImpl implements AtSecondaryServer {
secondaryPersistenceStore = SecondaryPersistenceStoreFactory.getInstance()
.getSecondaryPersistenceStore(currentAtSign)!;

// Initialize enrollment manager
enrollmentManager = EnrollmentManager(secondaryKeyStore);

//Commit Log Compaction
commitLogCompactionJobInstance =
AtCompactionJob(_commitLog, secondaryPersistenceStore);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ abstract class AbstractVerbHandler implements VerbHandler {
if (getVerb().requiresAuth() && !atConnectionMetadata.isAuthenticated) {
throw UnAuthenticatedException('Command cannot be executed without auth');
}
// This check verifies whether the enrollment is active on the already APKAM authenticated existing connection
// and terminates if the enrollment is expired.
// At this stage, the enrollmentId is not set to the InboundConnectionMetadata for the new connections.
// This will not terminate an un-authenticated connection when attempting to execute a PKAM verb with an expired enrollmentId.
(bool, Response) isEnrollmentActive =
await _verifyIfEnrollmentIsActive(response, atConnectionMetadata);
if (isEnrollmentActive.$1 == false) {
await atConnection.close();
return isEnrollmentActive.$2;
}
try {
// Parse the command
var verbParams = parse(command);
Expand All @@ -73,6 +83,52 @@ abstract class AbstractVerbHandler implements VerbHandler {
}
}

/// When authenticated with the APKAM keys, checks if the enrollment is active.
/// Returns true if the enrollment is active; otherwise, returns false.
Future<(bool, Response)> _verifyIfEnrollmentIsActive(
Response response, AtConnectionMetaData atConnectionMetadata) async {
// When authenticated with legacy keys, enrollment id is null. APKAM expiry does not
// apply to such connections. Therefore, return true.
if ((atConnectionMetadata as InboundConnectionMetadata).enrollmentId ==
null) {
logger.finest(
"Enrollment id is not found. Returning true from _verifyIfEnrollmentIsActive");
return (true, response);
}
try {
EnrollDataStoreValue enrollDataStoreValue =
await AtSecondaryServerImpl.getInstance()
.enrollmentManager
.get(atConnectionMetadata.enrollmentId!);
// If the enrollment status is expired, then the enrollment is not active. Return false.
if (enrollDataStoreValue.approval?.state ==
EnrollmentStatus.expired.name) {
logger.severe(
'The enrollment id: ${atConnectionMetadata.enrollmentId} is expired. Closing the connection');
response
..isError = true
..errorCode = 'AT0028'
..errorMessage =
'The enrollment id: ${(atConnectionMetadata).enrollmentId} is expired. Closing the connection';
return (false, response);
}
// The expired enrollments are removed from the keystore. In such cases, KeyNotFoundException is
// thrown. Return false.
} on KeyNotFoundException {
logger.severe(
'The enrollment id: ${atConnectionMetadata.enrollmentId} is expired. Closing the connection');
response
..isError = true
..errorCode = 'AT0028'
..errorMessage =
'The enrollment id: ${(atConnectionMetadata).enrollmentId} is expired. Closing the connection';
return (false, response);
}
logger.finest(
"Enrollment id ${atConnectionMetadata.enrollmentId} is active. Returning true from _verifyIfEnrollmentIsActive");
return (true, response);
}

/// Return the instance of the current verb
///@return instance of [Verb]
Verb getVerb();
Expand All @@ -93,9 +149,7 @@ 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 !=
EnrollmentStatus.approved.name) {
if (!SecondaryUtil.isActiveKey(enrollData)) {
enrollDataStoreValue.approval?.state = EnrollmentStatus.expired.name;
}
return enrollDataStoreValue;
Expand Down Expand Up @@ -144,21 +198,20 @@ abstract class AbstractVerbHandler implements VerbHandler {
return true;
}

// Step 1: From the enrollmentId fetch the enrollment details
final enrollmentKey = '${inboundConnectionMetadata.enrollmentId}'
'.$newEnrollmentKeyPattern'
'.$enrollManageNamespace'
'${AtSecondaryServerImpl.getInstance().currentAtSign}';
EnrollDataStoreValue enrollDataStoreValue;

try {
enrollDataStoreValue = await getEnrollDataStoreValue(enrollmentKey);
enrollDataStoreValue = await AtSecondaryServerImpl.getInstance()
.enrollmentManager
.get(inboundConnectionMetadata.enrollmentId!);
} on KeyNotFoundException {
logger.shout('Could not retrieve enrollment data for $enrollmentKey');
logger.shout(
'Could not retrieve enrollment data for ${inboundConnectionMetadata.enrollmentId}');
return false;
}

bool isValid = _applyEnrollmentValidations(
enrollDataStoreValue, enrollmentKey, operation, atKey, namespace);
enrollDataStoreValue, operation, atKey, namespace);
if (!isValid) {
return isValid;
}
Expand Down Expand Up @@ -254,18 +307,12 @@ abstract class AbstractVerbHandler implements VerbHandler {
return (authorisedNamespace, access);
}

bool _applyEnrollmentValidations(
EnrollDataStoreValue enrollDataStoreValue,
String enrollmentKey,
String operation,
String? atKey,
String? namespace) {
bool _applyEnrollmentValidations(EnrollDataStoreValue enrollDataStoreValue,
String operation, String? atKey, String? namespace) {
// Only approved enrollmentId is authorised to perform operations. Return false for enrollments
// which are not approved.
if (enrollDataStoreValue.approval?.state !=
EnrollmentStatus.approved.name) {
logger.warning('Enrollment state for $enrollmentKey'
' is ${enrollDataStoreValue.approval?.state}');
return false;
}
// Only the enrollmentId with access to "__manage" namespace can approve, deny, revoke
Expand Down
Loading

0 comments on commit 997dfbc

Please sign in to comment.