Skip to content

Commit 670027e

Browse files
Add replace-comment command (#1)
* add replace-comment command
1 parent a7eba6b commit 670027e

File tree

3 files changed

+180
-37
lines changed

3 files changed

+180
-37
lines changed

bin/main.ml

Lines changed: 105 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,27 @@ module Prompt = struct
5656
| "Y" | "y" -> Lwt.return (TTY true)
5757
| _ -> Lwt.return (TTY false))
5858

59-
let input_help_if_user_input () =
59+
let input_help_if_user_input ?(msg = "Please type the secret and then do Ctrl+d twice to terminate input") () =
6060
match is_TTY with
61-
| true -> Lwt_io.printl "I: reading from stdin. Please type the secret and then do Ctrl+d twice to terminate input"
61+
| true -> Lwt_io.printl @@ sprintf "I: reading from stdin. %s" msg
6262
| false -> Lwt.return_unit
6363

6464
let read_input_from_stdin ?initial:_ () = Lwt_io.(read stdin)
6565

66-
let rec input_and_validate_loop ?(transform = fun x -> x) ?initial get_secret_input =
66+
let validate_secret secret =
67+
match Secret.Validation.validate secret with
68+
| Error (e, _typ) -> Error e
69+
| _ -> Ok ()
70+
71+
let validate_comments comments =
72+
let comment_has_empty_lines =
73+
String.trim comments |> String.split_on_char '\n' |> List.map String.trim |> List.mem ""
74+
in
75+
match comment_has_empty_lines with
76+
| true -> Error "secrets cannot have empty lines in the middle of the comments"
77+
| false -> Ok ()
78+
79+
let rec input_and_validate_loop ~validate ?initial get_input =
6780
let remove_trailing_newlines s =
6881
(* reverse the string and count leading newlines instead of traversing the string
6982
multiple times to remove trailing newlines *)
@@ -81,29 +94,29 @@ module Prompt = struct
8194
let trailing_newlines = count_leading_newlines rev_s in
8295
String.sub s 0 (String.length s - trailing_newlines)
8396
in
84-
let%lwt input = get_secret_input ?initial () in
85-
let input = transform input in
97+
let%lwt input = get_input ?initial () in
8698
(* Remove bash commented lines from the secret and any trailing newlines *)
8799
let secret =
88100
String.split_on_char '\n' input
89101
|> List.filter (fun line -> not (String.starts_with ~prefix:"#" line))
90102
|> String.concat "\n"
91103
|> remove_trailing_newlines
92104
in
93-
match Secret.Validation.validate secret with
94-
| Error (e, _typ) ->
105+
match validate input with
106+
| Error e ->
95107
if is_TTY = false then Shell.die "This secret is in an invalid format: %s" e
96108
else (
97109
let%lwt () = Lwt_io.printlf "\nThis secret is in an invalid format: %s" e in
98-
if%lwt yesno "Edit again?" then input_and_validate_loop ~initial:input get_secret_input else Lwt.return_error e)
110+
if%lwt yesno "Edit again?" then input_and_validate_loop ~validate ~initial:input get_input
111+
else Lwt.return_error e)
99112
| _ -> Lwt.return_ok secret
100113

101114
(** Gets and validates user input reading from stdin. If the input has the wrong format, the user
102115
is prompted to reinput the secret with the correct format. Allows passing in a function for input
103116
transformation. Throws an error if the transformed input doesn't comply with the format and the
104117
user doesn't want to fix the input format. *)
105-
let get_valid_input_from_stdin_exn ?transform () =
106-
match%lwt input_and_validate_loop ?transform read_input_from_stdin with
118+
let get_valid_input_from_stdin_exn ?(validate = validate_secret) () =
119+
match%lwt input_and_validate_loop ~validate read_input_from_stdin with
107120
| Error e -> Shell.die "This secret is in an invalid format: %s" e
108121
| Ok secret -> Lwt.return_ok secret
109122
end
@@ -427,7 +440,8 @@ module Edit_cmd = struct
427440
Invariant.run_if_recipient ~op_string:"edit secret" ~path:(path_of_secret_name secret_name) ~f:(fun () ->
428441
Edit.edit_secret secret_name ~allow_retry:true ~get_updated_secret:(fun initial ->
429442
Prompt.input_and_validate_loop
430-
(* when we are editing a secret, we know `initial` is Some and we add the format explainer to it *)
443+
~validate:Prompt.validate_secret
444+
(* when we are editing a secret, we know `initial` is Some and we add the format explainer to it *)
431445
?initial:(Option.map (fun i -> i ^ Secret.format_explainer) initial)
432446
(Edit.new_text_from_editor ~name:(show_name secret_name))))
433447

@@ -802,7 +816,7 @@ module New = struct
802816
let create_new_secret secret_name =
803817
let%lwt () =
804818
Edit.edit_secret ~self_fallback:true secret_name ~allow_retry:true ~get_updated_secret:(fun initial ->
805-
Prompt.input_and_validate_loop
819+
Prompt.input_and_validate_loop ~validate:Prompt.validate_secret
806820
~initial:(Option.value ~default:Secret.format_explainer initial)
807821
(Edit.new_text_from_editor ~name:(show_name secret_name)))
808822
in
@@ -901,35 +915,24 @@ module Replace = struct
901915
| false -> "\n\n" ^ new_secret_plaintext)
902916
| true ->
903917
(* if there is already a secret, recreate or replace it *)
904-
let%lwt original_secret' =
905-
(* Get the original secret if we are in the recipient list, otherwise fully replace it *)
906-
try%lwt Storage.Secrets.decrypt_exn ~silence_stderr:true secret_name with _ -> Lwt.return ""
907-
in
908-
let original_secret =
909-
try Ok (Secret.Validation.parse_exn original_secret')
910-
with _e -> Error "failed to parse original secret"
911-
in
912-
let extract_comments ~f ~default secret =
913-
Result.map (fun ({ comments; _ } : Secret.t) -> Option.map f comments |> Option.value ~default) secret
914-
|> Result.value ~default
915-
in
916-
(* if the input doesn't have a newline char at the end we need to add one *)
917-
let new_secret_plaintext =
918-
match String.ends_with ~suffix:"\n" new_secret_plaintext with
919-
| true -> new_secret_plaintext
920-
| false -> new_secret_plaintext ^ "\n"
918+
let%lwt original_secret =
919+
try%lwt
920+
let%lwt secret_plaintext = Storage.Secrets.decrypt_exn ~silence_stderr:true secret_name in
921+
Lwt.return @@ Secret.Validation.parse_exn secret_plaintext
922+
with _e ->
923+
Shell.die
924+
"E: unable to parse secret %s's format. If we proceed, the comments will be lost. Aborting. Please \
925+
use the edit command to replace and fix this secret."
926+
(show_name secret_name)
921927
in
922928
Lwt.return
923929
(match is_singleline_secret with
924930
| true ->
925-
new_secret_plaintext
926-
^ extract_comments ~f:(fun comments -> "\n" ^ comments) ~default:"" original_secret
931+
Secret.singleline_from_text_description new_secret_plaintext
932+
(Option.value ~default:"" original_secret.comments)
927933
| false ->
928-
(* add an empty line before comments and before the secret,
929-
or just an empty line if there are no comments *)
930-
extract_comments ~f:(fun comments -> "\n" ^ comments ^ "\n") ~default:"\n" original_secret
931-
^ "\n"
932-
^ new_secret_plaintext)
934+
Secret.multiline_from_text_description new_secret_plaintext
935+
(Option.value ~default:"" original_secret.comments))
933936
in
934937
try%lwt Encrypt.encrypt_exn ~plaintext:updated_secret ~secret_name recipients
935938
with exn -> Shell.die ~exn "E: encrypting %s failed" (show_name secret_name))
@@ -944,6 +947,71 @@ module Replace = struct
944947
Cmd.v info term
945948
end
946949

950+
module Replace_comments = struct
951+
let replace_comment secret_name =
952+
let recipients = Storage.Secrets.(get_recipients_from_path_exn @@ to_path secret_name) in
953+
let secret_name_str = show_name secret_name in
954+
match recipients with
955+
| [] ->
956+
Shell.die
957+
{|E: No recipients found (use "passage {create,new} folder/new_secret_name" to use recipients associated with $PASSAGE_IDENTITY instead)|}
958+
secret_name_str
959+
| _ ->
960+
Invariant.run_if_recipient ~op_string:"replace comments" ~path:(path_of_secret_name secret_name) ~f:(fun () ->
961+
let%lwt updated_secret =
962+
match Storage.Secrets.secret_exists secret_name with
963+
| false -> Shell.die "E: no such secret: %s" secret_name_str
964+
| true ->
965+
let%lwt original_secret =
966+
try%lwt
967+
let%lwt original_secret_plaintext = Storage.Secrets.decrypt_exn ~silence_stderr:true secret_name in
968+
Lwt.return @@ Secret.Validation.parse_exn original_secret_plaintext
969+
with _e ->
970+
Shell.die
971+
"E: unable to parse secret %s's format. Please fix it before replacing the comments,or use the \
972+
edit command"
973+
secret_name_str
974+
in
975+
let get_comments_from_stdin () =
976+
let%lwt () =
977+
Prompt.input_help_if_user_input
978+
~msg:"Please type the new comments and then do Ctrl+d twice to terminate input" ()
979+
in
980+
let%lwt new_comments = Prompt.read_input_from_stdin () in
981+
match Prompt.validate_comments new_comments with
982+
| Error e -> Shell.die "The comments are in an invalid format: %s" e
983+
| _ -> Lwt.return new_comments
984+
in
985+
let get_comments_from_editor () =
986+
match%lwt
987+
Prompt.input_and_validate_loop ~validate:Prompt.validate_comments ?initial:original_secret.comments
988+
(Edit.new_text_from_editor ~name:(show_name secret_name))
989+
with
990+
| Error e -> Shell.die "The comments are in an invalid format: %s" e
991+
| Ok secret -> Lwt.return secret
992+
in
993+
let%lwt new_comments =
994+
match Prompt.is_TTY with
995+
| false -> get_comments_from_stdin ()
996+
| true -> get_comments_from_editor ()
997+
in
998+
let updated_secret =
999+
match original_secret.kind with
1000+
| Secret.Singleline -> Secret.singleline_from_text_description original_secret.text new_comments
1001+
| Secret.Multiline -> Secret.multiline_from_text_description original_secret.text new_comments
1002+
in
1003+
Lwt.return updated_secret
1004+
in
1005+
try%lwt Encrypt.encrypt_exn ~plaintext:updated_secret ~secret_name recipients
1006+
with exn -> Shell.die ~exn "E: encrypting %s failed" secret_name_str)
1007+
1008+
let replace_comments =
1009+
let doc = "replaces the comments of the specified secret, keeping the secret." in
1010+
let info = Cmd.info "replace-comment" ~doc in
1011+
let term = main_run Term.(const replace_comment $ Flags.secret_name) in
1012+
Cmd.v info term
1013+
end
1014+
9471015
module Rm = struct
9481016
let force =
9491017
let doc = "Delete secrets and folders without asking for confirmation" in
@@ -1268,6 +1336,7 @@ let () =
12681336
Realpath.realpath;
12691337
Refresh.refresh;
12701338
Replace.replace;
1339+
Replace_comments.replace_comments;
12711340
Rm.rm;
12721341
Search.search;
12731342
Get.secret;

lib/secret.ml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ let kind_to_string k =
1414
| Multiline -> "multi-line"
1515

1616
let singleline_from_text_description text description =
17+
let text = String.trim text in
1718
match description with
1819
| "" -> text
1920
| _ -> Printf.sprintf "%s\n\n%s" text description
2021

2122
let multiline_from_text_description text description =
23+
let text = String.trim text in
24+
let description = String.trim description in
2225
match description with
2326
| "" -> Printf.sprintf "\n\n%s" text
2427
| _ -> Printf.sprintf "\n%s\n\n%s" description text
@@ -83,7 +86,16 @@ module Validation = struct
8386
(* multi-line without comments *)
8487
| "" :: "" :: secret :: _ when String.trim secret <> "" -> Ok Multiline
8588
(* single-line with comments *)
86-
| secret :: "" :: _ when String.trim secret <> "" -> Ok Singleline
89+
| secret :: "" :: comments when String.trim secret <> "" ->
90+
let has_empty_lines_in_cmts =
91+
match comments with
92+
| [] -> false
93+
| cmts ->
94+
String.concat "\n" cmts |> String.trim |> String.split_on_char '\n' |> List.map String.trim |> List.mem ""
95+
in
96+
(match has_empty_lines_in_cmts with
97+
| true -> Error ("empty lines are not allowed in comments", InvalidFormat)
98+
| false -> Ok Singleline)
8799
(* We don't want to allow the creation of new secrets in legacy single-line format *)
88100
| secret :: comment :: _ when String.trim secret <> "" && String.trim comment <> "" ->
89101
Error

tests/replace_comment_command.t

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
$ . ./setup_fixtures.sh
2+
3+
Should fail - replace comments of a non-existing secret in a new folder - redirects to passage create or new
4+
$ echo "comment" | passage replace-comment folder/new
5+
E: No recipients found (use "passage {create,new} folder/new_secret_name" to use recipients associated with $PASSAGE_IDENTITY instead)
6+
[1]
7+
8+
Should fail - replace comments of a non-existing secret
9+
$ echo "comment" | passage replace-comment 00/secret2
10+
E: no such secret: 00/secret2
11+
[1]
12+
13+
Should succeed - replacing a the comments on a single-line secret without comments
14+
$ echo "replaced comments" | passage replace-comment 00/secret1
15+
$ passage cat 00/secret1
16+
(00/secret1) secret: single line
17+
18+
replaced comments
19+
20+
Should succeed - replacing a the comments on a single-line secret with comments
21+
$ echo "replaced again comments" | passage replace-comment 00/secret1
22+
$ passage cat 00/secret1
23+
(00/secret1) secret: single line
24+
25+
replaced again comments
26+
27+
Should succeed - replacing single-line comments with multiline comments
28+
$ echo "replaced again comments\nline 2" | passage replace-comment 00/secret1
29+
$ passage cat 00/secret1
30+
(00/secret1) secret: single line
31+
32+
replaced again comments
33+
line 2
34+
35+
Should succeed - replacing multiline comments with multiline comments
36+
$ echo "new comments\nline 2 of said new comments" | passage replace-comment 00/secret1
37+
$ passage cat 00/secret1
38+
(00/secret1) secret: single line
39+
40+
new comments
41+
line 2 of said new comments
42+
43+
Should succeed - replacing multiline comments with multiline comments - in multiline secret
44+
$ setup_multiline_secret_with_comments 00/secret2
45+
$ echo "new comments\nline 2 of said new comments" | passage replace-comment 00/secret2
46+
$ passage cat 00/secret2
47+
48+
new comments
49+
line 2 of said new comments
50+
51+
(00/secret2) secret: line 1
52+
(00/secret2) secret: line 2
53+
54+
Should fail - comments with empty lines in the middle
55+
$ echo "uno commento\n\ndos commentos" | passage replace-comment 00/secret1
56+
The comments are in an invalid format: secrets cannot have empty lines in the middle of the comments
57+
[1]
58+
$ passage cat 00/secret1
59+
(00/secret1) secret: single line
60+
61+
new comments
62+
line 2 of said new comments

0 commit comments

Comments
 (0)