-
Notifications
You must be signed in to change notification settings - Fork 11
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
feat: add 'auto' approve feature to auth_cli #685
Changes from 8 commits
5a5cae0
6a20f97
f9dd0ad
a279d0d
e2ec6ae
661015b
9e10db2
ebbf789
ff52013
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 |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import 'dart:async'; | ||
import 'dart:convert'; | ||
import 'dart:io'; | ||
|
||
|
@@ -20,10 +21,25 @@ final AtSignLogger logger = AtSignLogger(' CLI '); | |
|
||
final aca = AuthCliArgs(); | ||
|
||
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. We now create ephemeral storage each time; we clean it up when we exit |
||
Directory? storageDir; | ||
|
||
void deleteStorage() { | ||
// Windows will not let us delete files that are open | ||
// so will will ignore this step and leave them in %localappdata%\Temp | ||
if (!Platform.isWindows) { | ||
if (storageDir != null) { | ||
if (storageDir!.existsSync()) { | ||
// stderr.writeln('${DateTime.now()} : Cleaning up temporary files'); | ||
storageDir!.deleteSync(recursive: true); | ||
} | ||
} | ||
} | ||
} | ||
|
||
Future<int> main(List<String> arguments) async { | ||
AtSignLogger.defaultLoggingHandler = AtSignLogger.stdErrLoggingHandler; | ||
try { | ||
return await _main(arguments); | ||
return await wrappedMain(arguments); | ||
} on ArgumentError catch (e) { | ||
stderr.writeln('Invalid argument: ${e.message}'); | ||
aca.parser.printAllCommandsUsage(); | ||
|
@@ -32,10 +48,14 @@ Future<int> main(List<String> arguments) async { | |
stderr.writeln('Error: $e'); | ||
aca.parser.printAllCommandsUsage(); | ||
return 1; | ||
} finally { | ||
try { | ||
deleteStorage(); | ||
} catch (_) {} | ||
} | ||
} | ||
|
||
Future<int> _main(List<String> arguments) async { | ||
Future<int> wrappedMain(List<String> arguments) async { | ||
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. made public so that it can be called by the at_onboarding_cli_test in at_onboarding_cli_functional_tests |
||
if (arguments.isEmpty) { | ||
stderr.writeln('You must supply a command.'); | ||
aca.parser.printAllCommandsUsage(showSubCommandParams: false); | ||
|
@@ -157,6 +177,10 @@ Future<int> _main(List<String> arguments) async { | |
await approve( | ||
commandArgResults, await createAtClient(commandArgResults)); | ||
|
||
case AuthCliCommand.auto: | ||
await autoApprove( | ||
commandArgResults, await createAtClient(commandArgResults)); | ||
|
||
case AuthCliCommand.deny: | ||
await deny(commandArgResults, await createAtClient(commandArgResults)); | ||
|
||
|
@@ -248,16 +272,25 @@ Future<int> status(ArgResults ar) async { | |
} | ||
|
||
Future<AtClient> createAtClient(ArgResults ar) async { | ||
String nameSpace = 'at_auth_cli'; | ||
if (storageDir != null) { | ||
throw StateError('AtClient has already been created'); | ||
} | ||
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. Just a check to ensure we don't have a logic error elsewhere |
||
|
||
String nameSpace = 'at_activate'; | ||
String atSign = AtUtils.fixAtSign(ar[AuthCliArgs.argNameAtSign]); | ||
storageDir = standardAtClientStorageDir( | ||
atSign: atSign, | ||
progName: nameSpace, | ||
uniqueID: '${DateTime.now().millisecondsSinceEpoch}', | ||
); | ||
|
||
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. Using the utility function newly added to at_cli_commons 1.2.0 |
||
CLIBase cliBase = CLIBase( | ||
atSign: atSign, | ||
atKeysFilePath: ar[AuthCliArgs.argNameAtKeys], | ||
nameSpace: nameSpace, | ||
rootDomain: ar[AuthCliArgs.argNameAtDirectoryFqdn], | ||
homeDir: getHomeDirectory(), | ||
storageDir: '${getHomeDirectory()}/.atsign/$nameSpace/$atSign/storage' | ||
.replaceAll('/', Platform.pathSeparator), | ||
storageDir: storageDir!.path, | ||
verbose: ar[AuthCliArgs.argNameVerbose] || ar[AuthCliArgs.argNameDebug], | ||
syncDisabled: true, | ||
maxConnectAttempts: int.parse( | ||
|
@@ -474,6 +507,7 @@ Future<void> interactive(ArgResults argResults, AtClient atClient) async { | |
|
||
case AuthCliCommand.onboard: | ||
case AuthCliCommand.interactive: | ||
case AuthCliCommand.auto: | ||
case AuthCliCommand.status: | ||
case AuthCliCommand.enroll: | ||
stderr.writeln('The "${cliCommand.name}" command' | ||
|
@@ -650,7 +684,8 @@ Future<Map> _fetchOrListAndFilter( | |
return enrollmentMap; | ||
} | ||
|
||
Future<void> approve(ArgResults ar, AtClient atClient) async { | ||
Future<int> approve(ArgResults ar, AtClient atClient, {int? limit}) async { | ||
int approved = 0; | ||
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. Added an optional |
||
AtLookUp atLookup = atClient.getRemoteSecondary()!.atLookUp; | ||
|
||
Map toApprove = await _fetchOrListAndFilter( | ||
|
@@ -663,13 +698,15 @@ Future<void> approve(ArgResults ar, AtClient atClient) async { | |
|
||
if (toApprove.isEmpty) { | ||
stderr.writeln('No matching enrollment(s) found'); | ||
return; | ||
return approved; | ||
} | ||
|
||
// Iterate through the requests, approve each one | ||
for (String eId in toApprove.keys) { | ||
Map er = toApprove[eId]; | ||
stdout.writeln('Approving enrollmentId $eId'); | ||
stdout.writeln('Approving enrollmentId $eId' | ||
' with appName "${er['appName']}"' | ||
' and deviceName "${er['deviceName']}"'); | ||
// Then make a 'decision' object using data from the enrollment request | ||
EnrollmentRequestDecision decision = EnrollmentRequestDecision.approved( | ||
ApprovedRequestDecisionBuilder( | ||
|
@@ -680,9 +717,119 @@ Future<void> approve(ArgResults ar, AtClient atClient) async { | |
final response = await atAuthBase | ||
.atEnrollment(atClient.getCurrentAtSign()!) | ||
.approve(decision, atLookup); | ||
// 'enroll:approve:{"enrollmentId":"$enrollmentId"}' | ||
|
||
stdout.writeln('Server response: $response'); | ||
|
||
approved++; | ||
|
||
if (limit != null && approved >= limit) { | ||
return approved; | ||
} | ||
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. If we were passed a limit parameter (e.g. by the |
||
} | ||
return approved; | ||
} | ||
|
||
Future<int> autoApprove(ArgResults ar, AtClient atClient) async { | ||
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. New feature.
|
||
int approved = 0; | ||
int limit = int.parse(ar[AuthCliArgs.argNameLimit]); | ||
String? arx = ar[AuthCliArgs.argNameAppNameRegex]; | ||
String? drx = ar[AuthCliArgs.argNameDeviceNameRegex]; | ||
bool approveExisting = ar[AuthCliArgs.argNameAutoApproveExisting]; | ||
|
||
if (arx == null && drx == null) { | ||
throw IllegalArgumentException( | ||
'You must supply ${AuthCliArgs.argNameAppNameRegex}' | ||
' and/or ${AuthCliArgs.argNameDeviceNameRegex}'); | ||
} | ||
|
||
if (approveExisting) { | ||
// Start by approving any which match and are already there | ||
stdout.writeln('Approving any requests already there which are a match'); | ||
approved = await approve(ar, atClient, limit: limit); | ||
stdout.writeln(); | ||
} | ||
|
||
// If we've already approved our limit then we're done | ||
if (approved >= limit) { | ||
return approved; | ||
} | ||
|
||
Completer completer = Completer(); | ||
|
||
RegExp? appRegex; | ||
RegExp? deviceRegex; | ||
if (arx != null) { | ||
appRegex = RegExp(arx); | ||
} | ||
if (drx != null) { | ||
deviceRegex = RegExp(drx); | ||
} | ||
|
||
AtLookUp atLookup = atClient.getRemoteSecondary()!.atLookUp; | ||
|
||
// listen for enrollment requests | ||
stdout.writeln('Listening for new enrollment requests'); | ||
|
||
final stream = atClient.notificationService.subscribe( | ||
regex: r'.*\.new\.enrollments\.__manage', shouldDecrypt: false); | ||
|
||
final subscription = stream.listen((AtNotification n) async { | ||
if (completer.isCompleted) { | ||
return; // Don't handle any more if we're already done | ||
} | ||
|
||
String eId = n.key.substring(0, n.key.indexOf('.')); | ||
|
||
final er = jsonDecode(n.value!); | ||
stdout.writeln('Got enrollment request ID $eId' | ||
' with appName "${er['appName']}"' | ||
' and deviceName "${er['deviceName']}"'); | ||
|
||
// check the request matches our params | ||
String appName = er['appName']; | ||
String deviceName = er['deviceName']; | ||
if ((appRegex?.hasMatch(appName) ?? true) && | ||
(deviceRegex?.hasMatch(deviceName) ?? true)) { | ||
// request matched, let's approve it | ||
stdout.writeln('Approving enrollment request' | ||
' which matched the regex filters' | ||
' (app: "$arx" and device: "$drx" respectively)'); | ||
|
||
EnrollmentRequestDecision decision = EnrollmentRequestDecision.approved( | ||
ApprovedRequestDecisionBuilder( | ||
enrollmentId: eId, | ||
encryptedAPKAMSymmetricKey: er['encryptedAPKAMSymmetricKey'])); | ||
|
||
// Finally call approve method via an AtEnrollment object | ||
final response = await atAuthBase | ||
.atEnrollment(atClient.getCurrentAtSign()!) | ||
.approve(decision, atLookup); | ||
stdout.writeln('Approval successful.\n' | ||
'\tResponse: $response'); | ||
|
||
// increment approved count | ||
approved++; | ||
|
||
// check approved vs limit | ||
if (approved >= limit) { | ||
// if reached limit, complete the future | ||
stdout | ||
.writeln('Approved $approved requests - limit was $limit - done.'); | ||
completer.complete(); | ||
} | ||
} else { | ||
stdout.writeln('Ignoring enrollment request' | ||
' which does not match the regex filters' | ||
' (app: "$arx" and device: "$drx" respectively)'); | ||
} | ||
stdout.writeln(); | ||
}); | ||
|
||
// await future then cancel the subscription | ||
await completer.future; | ||
await subscription.cancel(); | ||
|
||
return approved; | ||
} | ||
|
||
Future<void> deny(ArgResults ar, AtClient atClient) async { | ||
|
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.
this file is deprecated as all functionality is now in cli/auth_cli.dart