diff --git a/at_commons/lib/src/at_constants.dart b/at_commons/lib/src/at_constants.dart index defba095..700bae07 100644 --- a/at_commons/lib/src/at_constants.dart +++ b/at_commons/lib/src/at_constants.dart @@ -71,3 +71,4 @@ const String statsNotificationId = '_latestNotificationIdv2'; const String ENCODING = 'encoding'; const String CLIENT_CONFIG = 'clientConfig'; const String VERSION = 'version'; +const String IS_LOCAL = 'isLocal'; diff --git a/at_commons/lib/src/keystore/at_key.dart b/at_commons/lib/src/keystore/at_key.dart index 28003cea..0ce57377 100644 --- a/at_commons/lib/src/keystore/at_key.dart +++ b/at_commons/lib/src/keystore/at_key.dart @@ -10,6 +10,10 @@ class AtKey { Metadata? metadata; bool isRef = false; + /// When set to true, represents the [LocalKey] + /// These keys will never be synced between the client and secondary server. + bool _isLocal = false; + String? get sharedBy => _sharedBy; set sharedBy(String? atSign) { @@ -24,6 +28,16 @@ class AtKey { _sharedWith = atSign; } + bool get isLocal => _isLocal; + + set isLocal(bool isLocal) { + if (isLocal == true && sharedWith != null) { + throw InvalidAtKeyException( + 'sharedWith should be empty when isLocal is set to true'); + } + _isLocal = isLocal; + } + String _dotNamespaceIfPresent() { if (namespace != null && namespace!.isNotEmpty) { return '.$namespace'; @@ -62,6 +76,14 @@ class AtKey { if (_sharedWith != null && _sharedWith!.isNotEmpty) { return '$_sharedWith:$key${_dotNamespaceIfPresent()}$_sharedBy'; } + // if key starts with local: or isLocal set to true, return local key + if (isLocal == true) { + String localKey = '$key${_dotNamespaceIfPresent()}$sharedBy'; + if (localKey.startsWith('local:')) { + return localKey; + } + return 'local:$localKey'; + } // Defaults to return a self key. return '$key${_dotNamespaceIfPresent()}$_sharedBy'; } @@ -151,6 +173,23 @@ class AtKey { ..namespace(namespace); } + /// Local key are confined to the client(device)/server it is created. + /// The key does not sync between the local-secondary and the cloud-secondary. + /// + /// Builds a local key and return a [LocalKeyBuilder]. + /// + /// Example: local:phone.wavi@alice + /// ```dart + /// AtKey localKey = AtKey.local('phone',namespace:'wavi').build(); + /// ``` + static LocalKeyBuilder local(String key, String sharedBy, + {String? namespace}) { + return LocalKeyBuilder() + ..key(key) + ..namespace(namespace) + ..sharedBy(sharedBy); + } + static AtKey fromString(String key) { var atKey = AtKey(); var metaData = Metadata(); @@ -180,6 +219,9 @@ class AtKey { if (keyParts[0] == 'public') { metaData.isPublic = true; } + if (keyParts[0] == 'local') { + atKey.isLocal = true; + } // Example key: cached:@alice:phone@bob else if (keyParts[0] == CACHED) { metaData.isCached = true; @@ -281,6 +323,21 @@ class PrivateKey extends AtKey { } } +/// Represents a local key +/// Local key are confined to the client(device)/server it is created. +/// The key does not sync between the local-secondary and the cloud-secondary. +class LocalKey extends AtKey { + LocalKey() { + isLocal = true; + super.metadata = Metadata(); + } + + @override + String toString() { + return 'local:$key${_dotNamespaceIfPresent()}$sharedBy'; + } +} + class Metadata { /// Represents the time in milliseconds beyond which the key expires int? ttl; diff --git a/at_commons/lib/src/keystore/at_key_builder_impl.dart b/at_commons/lib/src/keystore/at_key_builder_impl.dart index fe7df1f4..639d1929 100644 --- a/at_commons/lib/src/keystore/at_key_builder_impl.dart +++ b/at_commons/lib/src/keystore/at_key_builder_impl.dart @@ -131,3 +131,11 @@ class PrivateKeyBuilder extends AbstractKeyBuilder { _meta.isPublic = false; } } + +/// Builder to build the local keys +class LocalKeyBuilder extends AbstractKeyBuilder { + LocalKeyBuilder() : super() { + _atKey = LocalKey(); + _atKey.isLocal = true; + } +} diff --git a/at_commons/lib/src/keystore/key_type.dart b/at_commons/lib/src/keystore/key_type.dart index 58742c29..51afe55f 100644 --- a/at_commons/lib/src/keystore/key_type.dart +++ b/at_commons/lib/src/keystore/key_type.dart @@ -6,6 +6,7 @@ enum KeyType { cachedPublicKey, cachedSharedKey, reservedKey, + localKey, invalidKey } diff --git a/at_commons/lib/src/utils/at_key_regex_utils.dart b/at_commons/lib/src/utils/at_key_regex_utils.dart index 3c3da035..e6b32ccb 100644 --- a/at_commons/lib/src/utils/at_key_regex_utils.dart +++ b/at_commons/lib/src/utils/at_key_regex_utils.dart @@ -34,6 +34,8 @@ abstract class Regexes { '''(?(cached:public:){1})((@$sharedWithFragment)?$entityFragment'''; static const String reservedKeyFragment = '''(((@(?($charsInAtSign|$allowedEmoji){1,55}))|public|privatekey):)?(?$_charsInReservedKey)(@(?($charsInAtSign|$allowedEmoji){1,55}))?'''; + static const String localKeyFragment = + '''(?(local:){1})$entityFragment'''; String get publicKey; String get privateKey; @@ -42,6 +44,7 @@ abstract class Regexes { String get cachedSharedKey; String get cachedPublicKey; String get reservedKey; + String get localKey; static final Regexes _regexesWithMandatoryNamespace = RegexesWithMandatoryNamespace(); @@ -71,6 +74,8 @@ class RegexesWithMandatoryNamespace implements Regexes { '''${Regexes.cachedSharedKeyStartFragment}${Regexes.namespaceFragment}${Regexes.ownershipFragment}'''; static const String _cachedPublicKey = '''${Regexes.cachedPublicKeyStartFragment}${Regexes.namespaceFragment}${Regexes.ownershipFragment}'''; + static const String _localKey = + '''${Regexes.localKeyFragment}${Regexes.namespaceFragment}${Regexes.ownershipFragment}'''; @override String get publicKey => _publicKey; @@ -92,6 +97,9 @@ class RegexesWithMandatoryNamespace implements Regexes { @override String get reservedKey => Regexes.reservedKeyFragment; + + @override + String get localKey => _localKey; } class RegexesNonMandatoryNamespace implements Regexes { @@ -108,6 +116,8 @@ class RegexesNonMandatoryNamespace implements Regexes { '''${Regexes.cachedSharedKeyStartFragment}${Regexes.ownershipFragment}'''; static const String _cachedPublicKey = '''${Regexes.cachedPublicKeyStartFragment}${Regexes.ownershipFragment}'''; + static const String _localkey = + '''${Regexes.localKeyFragment}${Regexes.ownershipFragment}'''; @override String get publicKey => _publicKey; @@ -129,6 +139,9 @@ class RegexesNonMandatoryNamespace implements Regexes { @override String get reservedKey => Regexes.reservedKeyFragment; + + @override + String get localKey => _localkey; } class RegexUtil { @@ -168,6 +181,9 @@ class RegexUtil { if (matchAll(regexes.cachedSharedKey, key)) { return KeyType.cachedSharedKey; } + if (matchAll(regexes.localKey, key)) { + return KeyType.localKey; + } return KeyType.invalidKey; } diff --git a/at_commons/lib/src/validators/at_key_validation_impl.dart b/at_commons/lib/src/validators/at_key_validation_impl.dart index b6652a3b..87b35595 100644 --- a/at_commons/lib/src/validators/at_key_validation_impl.dart +++ b/at_commons/lib/src/validators/at_key_validation_impl.dart @@ -82,6 +82,9 @@ class _AtKeyValidatorImpl extends AtKeyValidator { case KeyType.reservedKey: _regex = regexes.reservedKey; break; + case KeyType.localKey: + _regex = regexes.localKey; + break; case KeyType.invalidKey: _regex = ''; break; diff --git a/at_commons/test/at_key_regex_test.dart b/at_commons/test/at_key_regex_test.dart index f44421aa..a82b396f 100644 --- a/at_commons/test/at_key_regex_test.dart +++ b/at_commons/test/at_key_regex_test.dart @@ -411,4 +411,28 @@ void main() { }); }); }); + + group('A group of test to validate local keys', () { + test('Test to validate local keys with enforcing namespaces', () { + var keyTypeList = []; + keyTypeList.add('local:phone.buzz@alice'); + keyTypeList.add('local:pho_-n________e.b@alice'); + keyTypeList.add('local:phone😀.buzz@alice💙'); + for (var key in keyTypeList) { + var type = RegexUtil.keyType(key, true); + expect(type == KeyType.localKey, true); + } + }); + + test('Test to validate local keys without enforcing namespaces', () { + var keyTypeList = []; + keyTypeList.add('local:phone@alice'); + keyTypeList.add('local:pho_-n________e@alice'); + keyTypeList.add('local:phone😀@alice💙'); + for (var key in keyTypeList) { + var type = RegexUtil.keyType(key, false); + expect(type == KeyType.localKey, true); + } + }); + }); } diff --git a/at_commons/test/at_key_test.dart b/at_commons/test/at_key_test.dart index 6f00e7cd..078cefd7 100644 --- a/at_commons/test/at_key_test.dart +++ b/at_commons/test/at_key_test.dart @@ -513,4 +513,102 @@ void main() { expect('privatekey:at_secret', atKey.toString()); }); }); + + group('A group of tests to validate local key', () { + test('A test to verify toString on AtKey', () { + var atKey = AtKey() + ..key = 'phone' + ..sharedBy = '@alice' + ..namespace = 'wavi' + ..isLocal = true; + expect(atKey.toString(), 'local:phone.wavi@alice'); + }); + + test('A test to verify toString on AtKey with local: in atKey', () { + var atKey = AtKey() + ..key = 'local:phone' + ..sharedBy = '@alice' + ..namespace = 'wavi' + ..isLocal = true; + expect(atKey.toString(), 'local:phone.wavi@alice'); + }); + + test('A test to verify fromString on AtKey', () { + var atKey = AtKey.fromString('local:phone.wavi@alice'); + expect(atKey.key, 'phone'); + expect(atKey.namespace, 'wavi'); + expect(atKey.sharedBy, '@alice'); + expect(atKey.isLocal, true); + }); + + test('A test to validate the creation of local key using static method', + () { + var atKey = AtKey.local('phone', '@alice', namespace: 'wavi').build(); + expect(atKey.key, 'phone'); + expect(atKey.namespace, 'wavi'); + expect(atKey.sharedBy, '@alice'); + expect(atKey.isLocal, true); + }); + + test( + 'A test to verify InvalidAtKey exception is thrown when sharedWith and isLocal are populated', + () { + expect( + () => AtKey() + ..key = 'phone' + ..namespace = 'wavi' + ..sharedWith = '@bob' + ..sharedBy = '@alice' + ..isLocal = true, + throwsA(predicate((dynamic e) => + e is InvalidAtKeyException && + e.message == + 'sharedWith should be empty when isLocal is set to true'))); + }); + + test('A test to verify local key builder', () { + var localKey = (LocalKeyBuilder() + ..key('phone') + ..sharedBy('@alice')) + .build(); + expect(localKey, isA()); + expect(localKey.isLocal, true); + expect(localKey.toString(), 'local:phone@alice'); + }); + + test('validate a local key with sharedBy populated', () { + var localKey = (LocalKeyBuilder() + ..key('phone') + ..sharedBy('@alice')) + .build(); + var validationResult = AtKeyValidators.get().validate( + localKey.toString(), ValidationContext()..atSign = '@alice'); + expect(validationResult.isValid, true); + }); + + test('validate a local key with sharedBy not populated', () { + var localKey = (LocalKeyBuilder() + ..key('phone') + ..sharedBy('')) + .build(); + var validationResult = AtKeyValidators.get().validate( + localKey.toString(), ValidationContext()..atSign = '@alice'); + expect(validationResult.isValid, false); + expect(validationResult.failureReason, 'local:phone is not a valid key'); + }); + + test('validate a local key with namespace not populated', () { + var localKey = (LocalKeyBuilder() + ..key('phone') + ..sharedBy('@alice')) + .build(); + var validationResult = AtKeyValidators.get().validate( + localKey.toString(), + ValidationContext() + ..atSign = '@alice' + ..enforceNamespace = true); + expect(validationResult.isValid, false); + expect(validationResult.failureReason, 'local:phone@alice is not a valid key'); + }); + }); } diff --git a/at_commons/test/at_key_type_test.dart b/at_commons/test/at_key_type_test.dart index 45a72ba5..09e4da12 100644 --- a/at_commons/test/at_key_type_test.dart +++ b/at_commons/test/at_key_type_test.dart @@ -27,6 +27,11 @@ void main() { var keyType = AtKey.getKeyType('@bob:phone.buzz@bob'); expect(keyType, equals(KeyType.selfKey)); }); + + test('Test to verify local key type with namespace', () { + var keyType = AtKey.getKeyType('local:latestNotification.wavi@bob', enforceNameSpace: true); + expect(keyType, equals(KeyType.localKey)); + }); }); group('A group of tests to check invalid key types', () { test('Test public key type without namespace', () { @@ -62,6 +67,10 @@ void main() { var keyType = AtKey.getKeyType('@bob:phone@bob', enforceNameSpace: true); expect(keyType, equals(KeyType.invalidKey)); }); + test('Test local key type with atsign and without namespace', () { + var keyType = AtKey.getKeyType('local:phone@bob', enforceNameSpace: true); + expect(keyType, equals(KeyType.invalidKey)); + }); }); group('A group of tests to check reserved key types', () {