-
Notifications
You must be signed in to change notification settings - Fork 16
feat: Noports CLI implementation #2304
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
base: trunk
Are you sure you want to change the base?
Changes from all commits
4b1083f
99a0d12
e84fd0c
7990ba9
6d726f2
2b5b727
75f4a85
060c86f
f45b60e
5b4a778
4a0d2c7
3d56f0e
7a01f69
d4638d9
0bbf4ce
a36b39e
6c88288
68b0a94
f4c2cac
f250cda
f3ecf17
93f0b41
324f2e6
bf3ccae
ed60b50
ad51fe4
8befdb2
984a5a0
f98b80a
6cfe1a9
5d8a5b8
5007ec7
8c7f4a3
d66b030
c6a1fa3
9588a2a
11c6307
d4dc431
cbce19d
fcad752
a441257
f7527bc
64ea4a6
eabd000
8ba58bc
492755c
12cedd7
3a7ddbd
90a00f6
402c948
8a4a9d7
c59f870
a882ef9
56faa5d
edd41b2
5070f49
0a3b19e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| library noports_core_activate; | ||
|
|
||
| // Implementations | ||
| export 'package:noports_core/src/commands/activate/activate_impl.dart'; | ||
| export 'package:noports_core/src/commands/issue_keys/issue_keys_impl.dart'; | ||
|
|
||
| // Utilities | ||
| export 'package:noports_core/src/commands/utils/console.dart'; | ||
| export 'package:noports_core/src/commands/utils/usage_messages.dart'; | ||
| export 'package:noports_core/src/common/help_requested_exception.dart'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import 'dart:io'; | ||
|
|
||
| import 'package:at_auth/at_auth.dart'; | ||
| import 'package:at_client/at_client.dart'; | ||
| import 'package:at_onboarding_cli/at_onboarding_cli.dart'; | ||
| import 'package:at_utils/at_logger.dart'; | ||
| import 'package:noports_core/commands.dart'; | ||
| import 'package:noports_core/src/commands/activate/activate_params.dart'; | ||
| import 'package:noports_core/utils.dart'; | ||
|
|
||
| class Activate { | ||
| final ActivateParams _params; | ||
|
|
||
| final AtOnboardingService _onboardingService; | ||
|
|
||
| final logger = AtSignLogger('Activate', loggingHandler: CLILoggingHandler()) | ||
| ..level = 'info'; | ||
|
|
||
| Activate(this._onboardingService, this._params); | ||
|
|
||
| factory Activate.fromArgs(List<String> args) { | ||
| if (args.isEmpty) { | ||
| throw ArgumentError('At least one argument is required.'); | ||
| } | ||
|
|
||
| ActivateParams params = ActivateParams.fromArgs(args); | ||
|
|
||
| final preference = AtOnboardingPreference() | ||
| ..cramSecret = params.cramSecret | ||
| ..atKeysFilePath = params.atKeysFilePath; | ||
| AtOnboardingService service = AtOnboardingServiceImpl( | ||
| params.atsign, | ||
| preference, | ||
| ); | ||
|
|
||
| return Activate(service, params); | ||
| } | ||
|
|
||
| /// Entry point for the activate command | ||
| Future<int> wrappedMain() async { | ||
| switch (_params.type) { | ||
| case ActivateType.cram: | ||
| return await cramAuthenticate(); | ||
| case ActivateType.enroll: | ||
| return await enroll(); | ||
| } | ||
| } | ||
|
|
||
| /// Authenticates an existing atSign using CRAM credentials | ||
| /// | ||
| /// Requires [_params.cramSecret] to be set | ||
| /// | ||
| /// Returns: 0 if authentication succeeds, 1 in case of failure | ||
| /// Throws: [ArgumentError] if cram credentials are missing | ||
| Future<int> cramAuthenticate() async { | ||
| if (_params.cramSecret == null) { | ||
| throw ArgumentError('Cannot perform CRAM auth without secret'); | ||
| } | ||
| logger.info('Activating atsign: ${_params.atsign}'); | ||
|
|
||
| final success = await _onboardingService.onboard(); | ||
|
|
||
| if (!success) { | ||
| logger.shout('Activation Failed'); | ||
| return 1; | ||
| } | ||
| logger.info('Activated'); | ||
| return 0; | ||
| } | ||
|
|
||
| /// Enrolls a new device using APKAM enrollment | ||
| /// | ||
| /// Requires [_params.otp] and [_params.device] to be set. | ||
| /// Optionally uses [_params.atKeysFilePath] if provided. | ||
| /// | ||
| /// Returns: [AtEnrollmentResponse] containing enrollment status and details | ||
| /// Throws: [ArgumentError] if otp is missing | ||
| Future<int> enroll() async { | ||
| if (_params.otp == null) { | ||
| throw ArgumentError('Cannot create enrollment without otp'); | ||
| } | ||
| logger.info( | ||
| 'Creating new enrollment with deviceName: ${_params.deviceName}', | ||
| ); | ||
|
|
||
| await _validateAndPrepareKeysFile(); | ||
|
|
||
| final response = await _onboardingService.enroll( | ||
| _params.appName, | ||
| _params.deviceName!, | ||
| _params.otp!, | ||
| _params.namespaces, | ||
| atKeysFile: _getKeysFile(), | ||
| ); | ||
|
|
||
| return response.enrollStatus == EnrollmentStatus.approved ? 0 : 1; | ||
| } | ||
|
|
||
| /// Validates and prepares the atKeys file location before enrollment. | ||
| /// | ||
| /// This method checks if a keys file already exists at the target location | ||
| /// (either specified by the user or at the default path) and prompts the user | ||
| /// to provide an alternate location if a file is found. This prevents | ||
| /// accidentally overwriting existing authentication keys during enrollment. | ||
| /// | ||
| /// The validation flow: | ||
| /// 1. If [_params.atKeysFilePath] is null, checks the default keys file path | ||
| /// from the [AtOnboardingPreference] | ||
| /// 2. If [_params.atKeysFilePath] is provided, checks that custom path | ||
| /// 3. If a file exists at either location, prompts the user for an alternate | ||
| /// path and updates [_params.atKeysFilePath] | ||
| /// 4. If no file exists, returns without modification (safe to proceed) | ||
| /// | ||
| /// | ||
| /// TODO: Replace [AtOnboardingServiceImpl] after ensuring that | ||
| /// [AtOnboardingService] throws a specific exception when keyFile exists at defaultPath | ||
| Future<void> _validateAndPrepareKeysFile() async { | ||
| final String? keysFilePath; | ||
|
|
||
| if (_params.atKeysFilePath == null) { | ||
| // Use the default path from onboarding service preferences | ||
| keysFilePath = (_onboardingService as AtOnboardingServiceImpl) | ||
| .atOnboardingPreference | ||
| .atKeysFilePath; | ||
| } else { | ||
| // Use the user-provided path | ||
| keysFilePath = _params.atKeysFilePath; | ||
| } | ||
|
|
||
| if (keysFilePath != null && !File(keysFilePath).existsSync()) { | ||
| return; | ||
| } | ||
|
|
||
| logger.shout('KeysFile exists at $keysFilePath'); | ||
| _params.atKeysFilePath = promptUser( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what happens if the user puts the same path in again? |
||
| 'Please provide alternate location to store keyfile', | ||
| ); | ||
| logger.info('Sending updated enrollment request'); | ||
| } | ||
|
|
||
| File? _getKeysFile() { | ||
| final path = _params.atKeysFilePath; | ||
| return path != null ? File(path) : null; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| import 'package:args/args.dart'; | ||
| import 'package:at_commons/atsign.dart'; | ||
| import 'package:noports_core/commands.dart'; | ||
| import 'package:noports_core/src/commands/utils/constants.dart'; | ||
| import 'package:noports_core/src/commands/utils/regex.dart'; | ||
|
|
||
| enum ActivateType { | ||
| cram, | ||
| enroll; | ||
|
|
||
| static ActivateType parse(String input) { | ||
| try { | ||
| return values.firstWhere((type) => input.contains(type.name)); | ||
| } on Exception { | ||
| throw ArgumentError( | ||
| 'Invalid activation type in: $input (expected "cram" or "enroll")', | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| class ActivateParams { | ||
| final String atsign; | ||
| final ActivateType type; | ||
| final String? cramSecret; | ||
| final String? otp; | ||
| final String? deviceName; | ||
| final appName = defaultAppName; | ||
| final namespaces = defaultEnrollmentNamespaces; | ||
|
|
||
| String? atKeysFilePath; | ||
|
|
||
| // Static parser for consistent usage and help generation | ||
| static final ArgParser argParser = _createArgParser(); | ||
|
|
||
| ActivateParams({ | ||
| required this.atsign, | ||
| required this.type, | ||
| this.cramSecret, | ||
| this.otp, | ||
| this.deviceName, | ||
| this.atKeysFilePath, | ||
| }); | ||
|
|
||
| factory ActivateParams.fromArgs(List<String> args) { | ||
| final results = argParser.parse(args); | ||
|
|
||
| if (results.wasParsed('help')) { | ||
| throw HelpRequestedException(); | ||
| } | ||
|
|
||
| if (results.rest.isEmpty) { | ||
| throw ArgumentError( | ||
| 'Activation string is required (e.g. @alice:cram:secret or' | ||
| ' @alice:enroll:otp:123456)', | ||
| ); | ||
| } | ||
|
|
||
| final activationString = results.rest.single; | ||
| final type = ActivateType.parse(activationString); | ||
|
|
||
| final atsign = _parseAtsign(activationString, type); | ||
| if (atsign == null || atsign.isEmpty) { | ||
| throw ArgumentError('atsign is required in activation string'); | ||
| } | ||
|
|
||
| // parse from arg parser results | ||
| final keyfile = results['target-keyfile'] as String?; | ||
|
|
||
| return ActivateParams( | ||
| atsign: atsign, | ||
| type: type, | ||
| cramSecret: _parseCramSecret(activationString), | ||
| otp: _parseOtp(activationString), | ||
| deviceName: _parseDeviceName(activationString), | ||
| atKeysFilePath: keyfile, | ||
| ); | ||
| } | ||
|
|
||
| static ArgParser _createArgParser() { | ||
| return ArgParser() | ||
| ..addOption( | ||
| 'target-keyfile', | ||
| abbr: 't', | ||
| mandatory: false, | ||
| help: 'Destination path for atKeys file', | ||
| ) | ||
| ..addFlag( | ||
| 'help', | ||
| abbr: 'h', | ||
| negatable: false, | ||
| help: 'Show this usage info', | ||
| ); | ||
| } | ||
|
|
||
| static String? _parseAtsign(String input, ActivateType type) { | ||
gkc marked this conversation as resolved.
Show resolved
Hide resolved
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return type should be |
||
| final regex = type == ActivateType.cram | ||
| ? ActivateRegex.cram | ||
| : ActivateRegex.enroll; | ||
|
|
||
| final match = regex.firstMatch(input); | ||
| final atsign = match?.namedGroup(ActivateRegexGroups.atsign); | ||
|
|
||
| return atsign != null && atsign.isNotEmpty ? atsign.toAtsign() : null; | ||
| } | ||
|
|
||
| static String? _parseCramSecret(String input) { | ||
| final match = ActivateRegex.cram.firstMatch(input); | ||
| return match?.namedGroup(ActivateRegexGroups.cram); | ||
| } | ||
|
|
||
| static String? _parseOtp(String input) { | ||
| final match = ActivateRegex.enroll.firstMatch(input); | ||
| return match?.namedGroup(ActivateRegexGroups.otp); | ||
| } | ||
|
|
||
| static String? _parseDeviceName(String input) { | ||
| final match = ActivateRegex.enroll.firstMatch(input); | ||
| return match?.namedGroup(ActivateRegexGroups.deviceName); | ||
| } | ||
|
|
||
| Map<String, String?> toJson() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are missing a test somewhere to ensure this kind of problem doesn't happen
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. after further discussion we concluded that toJson is no longer needed and can be removed from this class |
||
| return { | ||
| 'atsign': atsign, | ||
| 'cramSecret': cramSecret, | ||
| 'otp': otp, | ||
| 'deviceName': deviceName, | ||
| 'atKeysFilePath': atKeysFilePath, | ||
| }; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Workaround to avoid conflicts with creating atKeys files. Currently fetches the defaultPath from OnboardingServiceImpl.
Changes required: OnboardingService should throw a dedicated exception for key file name collisions, which can then be caught and handled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to address this issue fully in this PR. Find the places that MUST have file existence checking (onboard, enroll), ensure the checking always happens there. Remove file existence checking from elsewhere and verify that all the ways we create atKeys files do end up having a file existence check so we don't ever overwrite keys files. As a final safeguard, put a check into the place we finally write an AtKeys file and do not permit overwriting.