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

fix: Introduce time duration for apkam keys to auto expire #2085

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4ffe975
fix: Introduce time duration for apkam keys to auto expire
sitaram-kalluri Sep 4, 2024
4ab376c
fix: Use ?? for null check for apkamKeysExpiryDuration
sitaram-kalluri Sep 12, 2024
5d7edca
Merge remote-tracking branch 'origin/trunk' into 2074-introducing-aut…
sitaram-kalluri Sep 12, 2024
4c558ac
feat: Update dependent package versions and CHANGELOG.md
sitaram-kalluri Sep 13, 2024
9726357
Merge remote-tracking branch 'origin/trunk' into 2074-introducing-aut…
sitaram-kalluri Sep 13, 2024
bc14389
feat: Remove unnecessary null-aware operator
sitaram-kalluri Sep 13, 2024
49aef1d
Merge branch 'trunk' into 2074-introducing-auto-expiry-and-time-to-bi…
sitaram-kalluri Sep 16, 2024
90dff59
fix: Terminate the inbound connection when apkam keys are expired
sitaram-kalluri Sep 18, 2024
b5198a7
Merge remote-tracking branch 'origin/trunk' into 2074-introducing-aut…
sitaram-kalluri Sep 18, 2024
d8ca29e
fix: Add functional test to verify connection closes when apkam keys …
sitaram-kalluri Sep 19, 2024
1352ef0
fix: Add future delay to fix the failing unit test
sitaram-kalluri Sep 19, 2024
08ec802
Merge branch 'trunk' into 2074-introducing-auto-expiry-and-time-to-bi…
sitaram-kalluri Sep 19, 2024
8bbc86b
fix: Introduce EnrollmentManager class through which all interaction …
sitaram-kalluri Sep 23, 2024
ffea8d8
Merge remote-tracking branch 'origin/trunk' into 2074-introducing-aut…
sitaram-kalluri Sep 23, 2024
d6a0f4a
fix: Remove un-used variables from info_verb_handler.dart
sitaram-kalluri Sep 23, 2024
2516eff
Merge remote-tracking branch 'origin/trunk' into 2074-introducing-aut…
sitaram-kalluri Sep 24, 2024
7f9ff68
fix: Include EnrollmentManager changes into enrollment delete operati…
sitaram-kalluri Sep 24, 2024
a0fe5a7
fix: Add dart docs for the enrollment_manager.dart and remove comment…
sitaram-kalluri Sep 24, 2024
2c549f2
Merge branch 'trunk' into 2074-introducing-auto-expiry-and-time-to-bi…
sitaram-kalluri Sep 25, 2024
75b016c
Merge branch 'trunk' into 2074-introducing-auto-expiry-and-time-to-bi…
gkc Sep 25, 2024
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
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