diff --git a/packages/at_onboarding_cli/CHANGELOG.md b/packages/at_onboarding_cli/CHANGELOG.md index b16000ce..7f43592e 100644 --- a/packages/at_onboarding_cli/CHANGELOG.md +++ b/packages/at_onboarding_cli/CHANGELOG.md @@ -1,6 +1,7 @@ ## 1.8.0 - feat: add `unrevoke` command to the activate CLI - feat: add `delete` command to the activate CLI +- fix: When submitting an enrollment request, check for write permissions of AtKeys file path. ## 1.7.0 - feat: add `auto` command to the activate CLI ## 1.6.4 diff --git a/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart b/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart index 89f56677..ba62e6bf 100644 --- a/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart +++ b/packages/at_onboarding_cli/lib/src/cli/auth_cli.dart @@ -358,11 +358,24 @@ Future enroll(ArgResults argResults, {AtOnboardingService? svc}) async { } File f = File(argResults[AuthCliArgs.argNameAtKeys]); + if (f.existsSync()) { stderr.writeln('Error: atKeys file ${f.path} already exists'); return; } + // "canCreateFile" attempts to create the directories for the given [file] if they do not exist, + // and then tries to open the file in write mode to verify write permissions. + // + // If the file can be opened for writing, it is immediately closed and deleted. + // In [AtOnboardingServiceImpl._generateAtKeysFile] method, there is a check which returns error + // if the file already exists. Therefore, delete the file here after checking for write permissions. + // + // Incase of any exceptions, the error is logged and returned. + if (canCreateFile(f) == false) { + return; + } + svc ??= createOnboardingService(argResults); Map namespaces = {}; @@ -416,6 +429,51 @@ Future enroll(ArgResults argResults, {AtOnboardingService? svc}) async { } } +/// Checks if the specified [file] is writable. +/// +/// This function attempts to create the directories for the given [file] if they do not exist, +/// and then tries to open the file in write mode to verify write permissions. +/// If the file can be opened for writing, it is immediately closed and deleted. +/// +/// Returns: +/// - `true` if the file is writable, or +/// - `false` if the file is not writable due to existing files, lack of permissions, +/// or any other exceptions encountered during the process. +/// +/// [file]: The [File] instance to check for write access. +/// +/// Exceptions: +/// - If the file does not have write permissions, a [PathAccessException] is caught, and an error message is printed to stderr. +/// - Any other exceptions are caught and logged, indicating a failure to determine write access. +@visibleForTesting +bool canCreateFile(File file) { + try { + // If the directories do not exist, create them. + // "recursive" is set to true to ensure that any missing parent directories are created. + // "exclusive" is set to true to prevent creation of file if it already exists. + file.createSync(recursive: true, exclusive: true); + // Try opening the file in write mode, which requires write permissions + RandomAccessFile raf = file.openSync(mode: FileMode.write); + raf.closeSync(); + // In [AtOnboardingServiceImpl._generateAtKeysFile] method, there is a check which returns error + // if the file already exists. Therefore, delete the file here after checking for write permissions. + // This does not delete the existing file. Deletes only if the new file is created to verify write permissions. + file.deleteSync(); + return true; + } on PathExistsException { + stderr.writeln('Error : atKeys file ${file.path} already exists'); + rethrow; + } on PathAccessException { + stderr.writeln( + 'Error : atKeys file ${file.path} does not have write permissions'); + return false; + } catch (e) { + // If any exception occurs, we assume the file is not writable + stderr.writeln('Error in writing to atKeys file: ${e.toString()}'); + return false; + } +} + @visibleForTesting Future setSpp(ArgResults argResults, AtClient atClient) async { String spp = argResults[AuthCliArgs.argNameSpp]; diff --git a/packages/at_onboarding_cli/test/auth_cli_test.dart b/packages/at_onboarding_cli/test/auth_cli_test.dart new file mode 100644 index 00000000..beafb141 --- /dev/null +++ b/packages/at_onboarding_cli/test/auth_cli_test.dart @@ -0,0 +1,34 @@ +import 'dart:io'; + +import 'package:at_onboarding_cli/src/cli/auth_cli.dart'; +import 'package:test/test.dart'; + +void main() { + final baseDirPath = 'test/keys'; + group('A group of tests to verify write permission of apkam file path', () { + final dirPath = '$baseDirPath/@alice-apkam-keys.atKeys'; + + test( + 'A test to verify isWritable returns false if directory has read-only permissions', + () async { + final directory = Directory(dirPath); + // Create the directory first to ensure it exists before calling isWritable. + await directory.create(recursive: true); + // Set permission to read only. + await Process.run('chmod', ['444', baseDirPath]); + expect(canCreateFile(File(dirPath)), false); + }); + + test( + 'A test verify isWritable returns true if directory does not have a file already', + () { + expect(canCreateFile(File(dirPath)), true); + }); + }); + + tearDown(() async { + // Set full permissions to delete the directory. + await Process.run('chmod', ['777', baseDirPath]); + Directory(baseDirPath).deleteSync(recursive: true); + }); +}