Skip to content

Commit

Permalink
add replace-cmt command
Browse files Browse the repository at this point in the history
  • Loading branch information
thatportugueseguy committed Nov 22, 2024
1 parent 975ed63 commit b3b9e23
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 36 deletions.
138 changes: 103 additions & 35 deletions bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -56,30 +56,42 @@ module Prompt = struct
| "Y" | "y" -> Lwt.return (TTY true)
| _ -> Lwt.return (TTY false))

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

let read_input_from_stdin () = Lwt_io.(read stdin)

let rec input_and_validate_loop ?(transform = fun x -> x) get_secret_input =
let%lwt input = get_secret_input () in
let secret = transform input in
let validate_secret secret =
match Secret.Validation.validate secret with
| Error (e, _typ) ->
| Error (e, _typ) -> Error e
| _ -> Ok ()

let validate_comments comments =
let comment_has_empty_lines =
String.trim comments |> String.split_on_char '\n' |> List.map String.trim |> List.mem ""
in
match comment_has_empty_lines with
| true -> Error "secrets cannot have empty lines in the middle of the comments"
| false -> Ok ()

let rec input_and_validate_loop ~validate get_input =
let%lwt input = get_input () in
match validate input with
| Error e ->
if is_TTY = false then Shell.die "This secret is in an invalid format: %s" e
else (
let%lwt () = Lwt_io.printlf "\nThis secret is in an invalid format: %s" e in
if%lwt yesno "Edit again?" then input_and_validate_loop get_secret_input else Lwt.return_error e)
| _ -> Lwt.return_ok secret
if%lwt yesno "Edit again?" then input_and_validate_loop get_input ~validate else Lwt.return_error e)
| _ -> Lwt.return_ok input

(** Gets and validates user input reading from stdin. If the input has the wrong format, the user
is prompted to reinput the secret with the correct format. Allows passing in a function for input
transformation. Throws an error if the transformed input doesn't comply with the format and the
user doesn't want to fix the input format. *)
let get_valid_input_from_stdin_exn ?transform () =
match%lwt input_and_validate_loop ?transform read_input_from_stdin with
let get_valid_input_from_stdin_exn ?(validate = validate_secret) () =
match%lwt input_and_validate_loop ~validate read_input_from_stdin with
| Error e -> Shell.die "This secret is in an invalid format: %s" e
| Ok secret -> Lwt.return secret
end
Expand Down Expand Up @@ -402,7 +414,8 @@ module Edit_cmd = struct
Invariant.run_if_recipient ~op_string:"edit secret" ~path:(path_of_secret_name secret_name) ~f:(fun () ->
Edit.edit_secret secret_name ~allow_retry:true ~get_updated_secret:(fun initial ->
let%lwt secret =
Prompt.input_and_validate_loop (Edit.new_text_from_editor ?initial ~name:(show_name secret_name))
Prompt.input_and_validate_loop ~validate:Prompt.validate_secret
(Edit.new_text_from_editor ?initial ~name:(show_name secret_name))
in
Lwt.return (Result.value ~default:"" secret)))

Expand Down Expand Up @@ -937,35 +950,24 @@ module Replace = struct
| false -> "\n\n" ^ new_secret_plaintext)
| true ->
(* if there is already a secret, recreate or replace it *)
let%lwt original_secret' =
(* Get the original secret if we are in the recipient list, otherwise fully replace it *)
try%lwt Storage.Secrets.decrypt_exn ~silence_stderr:true secret_name with _ -> Lwt.return ""
in
let original_secret =
try Ok (Secret.Validation.parse_exn original_secret')
with _e -> Error "failed to parse original secret"
in
let extract_comments ~f ~default secret =
Result.map (fun ({ comments; _ } : Secret.t) -> Option.map f comments |> Option.value ~default) secret
|> Result.value ~default
in
(* if the input doesn't have a newline char at the end we need to add one *)
let new_secret_plaintext =
match String.ends_with ~suffix:"\n" new_secret_plaintext with
| true -> new_secret_plaintext
| false -> new_secret_plaintext ^ "\n"
let%lwt original_secret =
try%lwt
let%lwt secret_plaintext = Storage.Secrets.decrypt_exn ~silence_stderr:true secret_name in
Lwt.return @@ Secret.Validation.parse_exn secret_plaintext
with _e ->
Shell.die
"E: unable to parse secret %s's format. If we proceed, the comments will be lost. Aborting. Please \
use the edit command to replace and fix this secret."
(show_name secret_name)
in
Lwt.return
(match is_singleline_secret with
| true ->
new_secret_plaintext
^ extract_comments ~f:(fun comments -> "\n" ^ comments) ~default:"" original_secret
Secret.singleline_from_text_description new_secret_plaintext
(Option.value ~default:"" original_secret.comments)
| false ->
(* add an empty line before comments and before the secret,
or just an empty line if there are no comments *)
extract_comments ~f:(fun comments -> "\n" ^ comments ^ "\n") ~default:"\n" original_secret
^ "\n"
^ new_secret_plaintext)
Secret.multiline_from_text_description new_secret_plaintext
(Option.value ~default:"" original_secret.comments))
in
try%lwt Encrypt.encrypt_exn ~plaintext:updated_secret ~secret_name recipients
with exn -> Shell.die ~exn "E: encrypting %s failed" (show_name secret_name))
Expand All @@ -980,6 +982,71 @@ module Replace = struct
Cmd.v info term
end

module Replace_comments = struct
let replace_comment secret_name =
let recipients = Storage.Secrets.(get_recipients_from_path_exn @@ to_path secret_name) in
let secret_name_str = show_name secret_name in
match recipients with
| [] ->
Shell.die
{|E: No recipients found (use "passage {create,new} folder/new_secret_name" to use recipients associated with $PASSAGE_IDENTITY instead)|}
secret_name_str
| _ ->
Invariant.run_if_recipient ~op_string:"replace comments" ~path:(path_of_secret_name secret_name) ~f:(fun () ->
let%lwt updated_secret =
match Storage.Secrets.secret_exists secret_name with
| false -> Shell.die "E: no such secret: %s" secret_name_str
| true ->
let%lwt original_secret =
try%lwt
let%lwt original_secret_plaintext = Storage.Secrets.decrypt_exn ~silence_stderr:true secret_name in
Lwt.return @@ Secret.Validation.parse_exn original_secret_plaintext
with _e ->
Shell.die
"E: unable to parse secret %s's format. Please fix it before replacing the comments,or use the \
edit command"
secret_name_str
in
let get_comments_from_stdin () =
let%lwt () =
Prompt.input_help_if_user_input
~msg:"Please type the new comments and then do Ctrl+d twice to terminate input" ()
in
let%lwt new_comments = Prompt.read_input_from_stdin () in
match Prompt.validate_comments new_comments with
| Error e -> Shell.die "The comments are in an invalid format: %s" e
| _ -> Lwt.return new_comments
in
let get_comments_from_editor () =
match%lwt
Prompt.input_and_validate_loop ~validate:Prompt.validate_comments
(Edit.new_text_from_editor ?initial:original_secret.comments ~name:(show_name secret_name))
with
| Error e -> Shell.die "The comments are in an invalid format: %s" e
| Ok secret -> Lwt.return secret
in
let%lwt new_comments =
match Prompt.is_TTY with
| false -> get_comments_from_stdin ()
| true -> get_comments_from_editor ()
in
let updated_secret =
match original_secret.kind with
| Secret.Singleline -> Secret.singleline_from_text_description original_secret.text new_comments
| Secret.Multiline -> Secret.multiline_from_text_description original_secret.text new_comments
in
Lwt.return updated_secret
in
try%lwt Encrypt.encrypt_exn ~plaintext:updated_secret ~secret_name recipients
with exn -> Shell.die ~exn "E: encrypting %s failed" secret_name_str)

let replace_comments =
let doc = "replaces the comments of the specified secret, keeping the secret." in
let info = Cmd.info "replace-cmt" ~doc in
let term = main_run Term.(const replace_comment $ Flags.secret_name) in
Cmd.v info term
end

module Rm = struct
let force =
let doc = "Delete secrets and folders without asking for confirmation" in
Expand Down Expand Up @@ -1304,6 +1371,7 @@ let () =
Realpath.realpath;
Refresh.refresh;
Replace.replace;
Replace_comments.replace_comments;
Rm.rm;
Search.search;
Get.secret;
Expand Down
14 changes: 13 additions & 1 deletion lib/secret.ml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ let kind_to_string k =
| Multiline -> "multi-line"

let singleline_from_text_description text description =
let text = String.trim text in
match description with
| "" -> text
| _ -> Printf.sprintf "%s\n\n%s" text description

let multiline_from_text_description text description =
let text = String.trim text in
let description = String.trim description in
match description with
| "" -> Printf.sprintf "\n\n%s" text
| _ -> Printf.sprintf "\n%s\n\n%s" description text
Expand Down Expand Up @@ -57,7 +60,16 @@ module Validation = struct
(* multi-line without comments *)
| "" :: "" :: secret :: _ when String.trim secret <> "" -> Ok Multiline
(* single-line with comments *)
| secret :: "" :: _ when String.trim secret <> "" -> Ok Singleline
| secret :: "" :: comments when String.trim secret <> "" ->
let has_empty_lines_in_cmts =
match comments with
| [] -> false
| cmts ->
String.concat "\n" cmts |> String.trim |> String.split_on_char '\n' |> List.map String.trim |> List.mem ""
in
(match has_empty_lines_in_cmts with
| true -> Error ("empty lines are not allowed in comments", InvalidFormat)
| false -> Ok Singleline)
(* We don't want to allow the creation of new secrets in legacy single-line format *)
| secret :: comment :: _ when String.trim secret <> "" && String.trim comment <> "" ->
Error ("legacy single line secret format. Please use the correct format", SingleLineLegacy)
Expand Down
62 changes: 62 additions & 0 deletions tests/replace_cmt_command.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
$ . ./setup_fixtures.sh

Should fail - replace comments of a non-existing secret in a new folder - redirects to passage create or new
$ echo "comment" | passage replace-cmt folder/new
E: No recipients found (use "passage {create,new} folder/new_secret_name" to use recipients associated with $PASSAGE_IDENTITY instead)
[1]

Should fail - replace comments of a non-existing secret
$ echo "comment" | passage replace-cmt 00/secret2
E: no such secret: 00/secret2
[1]

Should succeed - replacing a the comments on a single-line secret without comments
$ echo "replaced comments" | passage replace-cmt 00/secret1
$ passage cat 00/secret1
(00/secret1) secret: single line

replaced comments

Should succeed - replacing a the comments on a single-line secret with comments
$ echo "replaced again comments" | passage replace-cmt 00/secret1
$ passage cat 00/secret1
(00/secret1) secret: single line

replaced again comments

Should succeed - replacing single-line comments with multiline comments
$ echo "replaced again comments\nline 2" | passage replace-cmt 00/secret1
$ passage cat 00/secret1
(00/secret1) secret: single line

replaced again comments
line 2

Should succeed - replacing multiline comments with multiline comments
$ echo "new comments\nline 2 of said new comments" | passage replace-cmt 00/secret1
$ passage cat 00/secret1
(00/secret1) secret: single line

new comments
line 2 of said new comments

Should succeed - replacing multiline comments with multiline comments - in multiline secret
$ setup_multiline_secret_with_comments 00/secret2
$ echo "new comments\nline 2 of said new comments" | passage replace-cmt 00/secret2
$ passage cat 00/secret2

new comments
line 2 of said new comments

(00/secret2) secret: line 1
(00/secret2) secret: line 2

Should fail - comments with empty lines in the middle
$ echo "uno commento\n\ndos commentos" | passage replace-cmt 00/secret1
The comments are in an invalid format: secrets cannot have empty lines in the middle of the comments
[1]
$ passage cat 00/secret1
(00/secret1) secret: single line

new comments
line 2 of said new comments

0 comments on commit b3b9e23

Please sign in to comment.