Browse Source

[clap-v3-utils] Deprecate signer source validation (#33802)

* make `SignerSource` implement `Clone`

* add `SignerSourceParserBuilder`

* deprecate `is_keypair`

* deprecate `is_keypair_or_ask_keyword`

* deprecate `is_prompt_signer_source`

* deprecate `is_pubkey_or_keypair`, `is_valid_pubkey`, and `is_valid_signer`

* deprecate `is_pubkey`

* bump deprecation version to 1.17.2

* temporarily allow deprecation for build

* Apply suggestions from code review

Co-authored-by: Tyera <teulberg@gmail.com>

* fix typo `SignerSourceParseBuilder` --> `SignerSourceParserBuilder`

* add `allow_` prefix to `SignerSourceParserBuilder` fields

* remove `SignerSourceParserBuilder::new()` and replace it with `Default`

* Update keygen/src/keygen.rs

Co-authored-by: Trent Nelson <trent.a.b.nelson@gmail.com>

* update deprecated version to `1.18.0`

---------

Co-authored-by: Tyera <teulberg@gmail.com>
Co-authored-by: Trent Nelson <trent.a.b.nelson@gmail.com>
samkim-crypto 1 year ago
parent
commit
6a9a89064b

+ 1 - 0
clap-v3-utils/src/fee_payer.rs

@@ -11,6 +11,7 @@ pub const FEE_PAYER_ARG: ArgConstant<'static> = ArgConstant {
            is also passed. Defaults to the client keypair.",
 };
 
+#[allow(deprecated)]
 pub fn fee_payer_arg<'a>() -> Arg<'a> {
     Arg::new(FEE_PAYER_ARG.name)
         .long(FEE_PAYER_ARG.long)

+ 336 - 2
clap-v3-utils/src/input_parsers/signer.rs

@@ -1,9 +1,12 @@
 use {
     crate::{
         input_parsers::{keypair_of, keypairs_of, pubkey_of, pubkeys_of},
-        keypair::{pubkey_from_path, resolve_signer_from_path, signer_from_path},
+        keypair::{
+            parse_signer_source, pubkey_from_path, resolve_signer_from_path, signer_from_path,
+            SignerSource, SignerSourceError, SignerSourceKind,
+        },
     },
-    clap::ArgMatches,
+    clap::{builder::ValueParser, ArgMatches},
     solana_remote_wallet::remote_wallet::RemoteWalletManager,
     solana_sdk::{
         pubkey::Pubkey,
@@ -15,6 +18,77 @@ use {
 // Sentinel value used to indicate to write to screen instead of file
 pub const STDOUT_OUTFILE_TOKEN: &str = "-";
 
+#[derive(Debug, Default)]
+pub struct SignerSourceParserBuilder {
+    allow_prompt: bool,
+    allow_file_path: bool,
+    allow_usb: bool,
+    allow_stdin: bool,
+    allow_pubkey: bool,
+    allow_legacy: bool,
+}
+
+impl SignerSourceParserBuilder {
+    pub fn allow_all(mut self) -> Self {
+        self.allow_prompt = true;
+        self.allow_file_path = true;
+        self.allow_usb = true;
+        self.allow_stdin = true;
+        self.allow_pubkey = true;
+        self.allow_legacy = true;
+        self
+    }
+
+    pub fn allow_prompt(mut self) -> Self {
+        self.allow_prompt = true;
+        self
+    }
+
+    pub fn allow_file_path(mut self) -> Self {
+        self.allow_file_path = true;
+        self
+    }
+
+    pub fn allow_usb(mut self) -> Self {
+        self.allow_usb = true;
+        self
+    }
+
+    pub fn allow_stdin(mut self) -> Self {
+        self.allow_stdin = true;
+        self
+    }
+
+    pub fn allow_pubkey(mut self) -> Self {
+        self.allow_pubkey = true;
+        self
+    }
+
+    pub fn allow_legacy(mut self) -> Self {
+        self.allow_legacy = true;
+        self
+    }
+
+    pub fn build(self) -> ValueParser {
+        ValueParser::from(
+            move |arg: &str| -> Result<SignerSource, SignerSourceError> {
+                let signer_source = parse_signer_source(arg)?;
+                if !self.allow_legacy && signer_source.legacy {
+                    return Err(SignerSourceError::UnsupportedSource);
+                }
+                match signer_source.kind {
+                    SignerSourceKind::Prompt if self.allow_prompt => Ok(signer_source),
+                    SignerSourceKind::Filepath(_) if self.allow_file_path => Ok(signer_source),
+                    SignerSourceKind::Usb(_) if self.allow_usb => Ok(signer_source),
+                    SignerSourceKind::Stdin if self.allow_stdin => Ok(signer_source),
+                    SignerSourceKind::Pubkey(_) if self.allow_pubkey => Ok(signer_source),
+                    _ => Err(SignerSourceError::UnsupportedSource),
+                }
+            },
+        )
+    }
+}
+
 // Return the keypair for an argument with filename `name` or `None` if not present wrapped inside `Result`.
 pub fn try_keypair_of(
     matches: &ArgMatches,
@@ -164,9 +238,11 @@ impl FromStr for PubkeySignature {
 mod tests {
     use {
         super::*,
+        assert_matches::assert_matches,
         clap::{Arg, Command},
         solana_sdk::signature::write_keypair_file,
         std::fs,
+        tempfile::NamedTempFile,
     };
 
     fn app<'ab>() -> Command<'ab> {
@@ -301,4 +377,262 @@ mod tests {
             .unwrap_err();
         assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
     }
+
+    #[test]
+    fn test_parse_keypair_source() {
+        let command = Command::new("test").arg(
+            Arg::new("keypair")
+                .long("keypair")
+                .takes_value(true)
+                .value_parser(
+                    SignerSourceParserBuilder::default()
+                        .allow_file_path()
+                        .build(),
+                ),
+        );
+
+        // success case
+        let file0 = NamedTempFile::new().unwrap();
+        let path = file0.path();
+        let path_str = path.to_str().unwrap();
+
+        let matches = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", path_str])
+            .unwrap();
+
+        let signer_source = matches.get_one::<SignerSource>("keypair").unwrap();
+
+        assert!(matches!(signer_source, SignerSource {
+                kind: SignerSourceKind::Filepath(p),
+                derivation_path: None,
+                legacy: false,
+            }
+            if p == path_str));
+
+        // failure cases
+        let matches_error = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "-"])
+            .unwrap_err();
+        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
+
+        let matches_error = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "usb://ledger"])
+            .unwrap_err();
+        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
+    }
+
+    #[test]
+    fn test_parse_keypair_or_ask_keyword_source() {
+        // allow `ASK` keyword
+        let command = Command::new("test").arg(
+            Arg::new("keypair")
+                .long("keypair")
+                .takes_value(true)
+                .value_parser(
+                    SignerSourceParserBuilder::default()
+                        .allow_file_path()
+                        .allow_prompt()
+                        .allow_legacy()
+                        .build(),
+                ),
+        );
+
+        // success cases
+        let file0 = NamedTempFile::new().unwrap();
+        let path = file0.path();
+        let path_str = path.to_str().unwrap();
+
+        let matches = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", path_str])
+            .unwrap();
+        let signer_source = matches.get_one::<SignerSource>("keypair").unwrap();
+        assert!(matches!(signer_source, SignerSource {
+                kind: SignerSourceKind::Filepath(p),
+                derivation_path: None,
+                legacy: false,
+            }
+            if p == path_str));
+
+        let matches = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "ASK"])
+            .unwrap();
+        let signer_source = matches.get_one::<SignerSource>("keypair").unwrap();
+        assert_matches!(
+            signer_source,
+            SignerSource {
+                kind: SignerSourceKind::Prompt,
+                derivation_path: None,
+                legacy: true,
+            }
+        );
+
+        let matches = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "prompt:"])
+            .unwrap();
+        let signer_source = matches.get_one::<SignerSource>("keypair").unwrap();
+        assert_matches!(
+            signer_source,
+            SignerSource {
+                kind: SignerSourceKind::Prompt,
+                derivation_path: None,
+                legacy: false,
+            }
+        );
+
+        // failure cases
+        let matches_error = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "-"])
+            .unwrap_err();
+        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
+
+        let matches_error = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "usb://ledger"])
+            .unwrap_err();
+        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
+
+        // disallow `ASK` keyword
+        let command = Command::new("test").arg(
+            Arg::new("keypair")
+                .long("keypair")
+                .takes_value(true)
+                .value_parser(
+                    SignerSourceParserBuilder::default()
+                        .allow_file_path()
+                        .allow_prompt()
+                        .build(),
+                ),
+        );
+
+        let matches_error = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "ASK"])
+            .unwrap_err();
+        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
+    }
+
+    #[test]
+    fn test_parse_prompt_signer_source() {
+        let command = Command::new("test").arg(
+            Arg::new("keypair")
+                .long("keypair")
+                .takes_value(true)
+                .value_parser(
+                    SignerSourceParserBuilder::default()
+                        .allow_prompt()
+                        .allow_legacy()
+                        .build(),
+                ),
+        );
+
+        // success case
+        let matches = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "ASK"])
+            .unwrap();
+        let signer_source = matches.get_one::<SignerSource>("keypair").unwrap();
+        assert_matches!(
+            signer_source,
+            SignerSource {
+                kind: SignerSourceKind::Prompt,
+                derivation_path: None,
+                legacy: true,
+            }
+        );
+
+        let matches = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "prompt:"])
+            .unwrap();
+        let signer_source = matches.get_one::<SignerSource>("keypair").unwrap();
+        assert_matches!(
+            signer_source,
+            SignerSource {
+                kind: SignerSourceKind::Prompt,
+                derivation_path: None,
+                legacy: false,
+            }
+        );
+
+        // failure cases
+        let matches_error = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "-"])
+            .unwrap_err();
+        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
+
+        let matches_error = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--keypair", "usb://ledger"])
+            .unwrap_err();
+        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
+    }
+
+    #[test]
+    fn test_parse_pubkey_or_keypair_signer_source() {
+        let command = Command::new("test").arg(
+            Arg::new("signer")
+                .long("signer")
+                .takes_value(true)
+                .value_parser(
+                    SignerSourceParserBuilder::default()
+                        .allow_pubkey()
+                        .allow_file_path()
+                        .build(),
+                ),
+        );
+
+        // success cases
+        let pubkey = Pubkey::new_unique();
+        let matches = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--signer", &pubkey.to_string()])
+            .unwrap();
+        let signer_source = matches.get_one::<SignerSource>("signer").unwrap();
+        assert!(matches!(
+            signer_source,
+            SignerSource {
+                kind: SignerSourceKind::Pubkey(p),
+                derivation_path: None,
+                legacy: false,
+            }
+            if *p == pubkey));
+
+        let file0 = NamedTempFile::new().unwrap();
+        let path = file0.path();
+        let path_str = path.to_str().unwrap();
+        let matches = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--signer", path_str])
+            .unwrap();
+        let signer_source = matches.get_one::<SignerSource>("signer").unwrap();
+        assert!(matches!(
+            signer_source,
+            SignerSource {
+                kind: SignerSourceKind::Filepath(p),
+                derivation_path: None,
+                legacy: false,
+            }
+            if p == path_str));
+
+        // failure cases
+        let matches_error = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--signer", "-"])
+            .unwrap_err();
+        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
+
+        let matches_error = command
+            .clone()
+            .try_get_matches_from(vec!["test", "--signer", "usb://ledger"])
+            .unwrap_err();
+        assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation);
+    }
 }

+ 32 - 0
clap-v3-utils/src/input_validators.rs

@@ -59,6 +59,10 @@ where
 }
 
 // Return an error if a pubkey cannot be parsed.
+#[deprecated(
+    since = "1.18.0",
+    note = "please use `clap::value_parser!(Pubkey)` instead"
+)]
 pub fn is_pubkey(string: &str) -> Result<(), String> {
     is_parsable_generic::<Pubkey, _>(string)
 }
@@ -76,6 +80,10 @@ where
 }
 
 // Return an error if a keypair file cannot be parsed.
+#[deprecated(
+    since = "1.18.0",
+    note = "please use `SignerSourceParserBuilder::default().allow_file_path().build()` instead"
+)]
 pub fn is_keypair<T>(string: T) -> Result<(), String>
 where
     T: AsRef<str> + Display,
@@ -86,6 +94,10 @@ where
 }
 
 // Return an error if a keypair file cannot be parsed
+#[deprecated(
+    since = "1.18.0",
+    note = "please use `SignerSourceParserBuilder::default().allow_file_path().allow_prompt().allow_legacy().build()` instead"
+)]
 pub fn is_keypair_or_ask_keyword<T>(string: T) -> Result<(), String>
 where
     T: AsRef<str> + Display,
@@ -99,6 +111,10 @@ where
 }
 
 // Return an error if a `SignerSourceKind::Prompt` cannot be parsed
+#[deprecated(
+    since = "1.18.0",
+    note = "please use `SignerSourceParserBuilder::default().allow_prompt().allow_legacy().build()` instead"
+)]
 pub fn is_prompt_signer_source(string: &str) -> Result<(), String> {
     if string == ASK_KEYWORD {
         return Ok(());
@@ -115,6 +131,11 @@ pub fn is_prompt_signer_source(string: &str) -> Result<(), String> {
 }
 
 // Return an error if string cannot be parsed as pubkey string or keypair file location
+#[deprecated(
+    since = "1.18.0",
+    note = "please use `SignerSourceParserBuilder::default().allow_pubkey().allow_file_path().build()` instead"
+)]
+#[allow(deprecated)]
 pub fn is_pubkey_or_keypair<T>(string: T) -> Result<(), String>
 where
     T: AsRef<str> + Display,
@@ -124,6 +145,11 @@ where
 
 // Return an error if string cannot be parsed as a pubkey string, or a valid Signer that can
 // produce a pubkey()
+#[deprecated(
+    since = "1.18.0",
+    note = "please use `SignerSourceParserBuilder::default().allow_pubkey().allow_file_path().build()` instead"
+)]
+#[allow(deprecated)]
 pub fn is_valid_pubkey<T>(string: T) -> Result<(), String>
 where
     T: AsRef<str> + Display,
@@ -145,6 +171,11 @@ where
 // when paired with an offline `--signer` argument to provide a Presigner (pubkey + signature).
 // Clap validators can't check multiple fields at once, so the verification that a `--signer` is
 // also provided and correct happens in parsing, not in validation.
+#[deprecated(
+    since = "1.18.0",
+    note = "please use `SignerSourceParserBuilder::default().build()` instead"
+)]
+#[allow(deprecated)]
 pub fn is_valid_signer<T>(string: T) -> Result<(), String>
 where
     T: AsRef<str> + Display,
@@ -157,6 +188,7 @@ where
     since = "1.17.0",
     note = "please use `clap::value_parser!(PubkeySignature)` instead"
 )]
+#[allow(deprecated)]
 pub fn is_pubkey_sig<T>(string: T) -> Result<(), String>
 where
     T: AsRef<str> + Display,

+ 4 - 1
clap-v3-utils/src/keypair.rs

@@ -371,7 +371,7 @@ impl DefaultSigner {
     }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Clone)]
 pub(crate) struct SignerSource {
     pub kind: SignerSourceKind,
     pub derivation_path: Option<DerivationPath>,
@@ -402,6 +402,7 @@ const SIGNER_SOURCE_USB: &str = "usb";
 const SIGNER_SOURCE_STDIN: &str = "stdin";
 const SIGNER_SOURCE_PUBKEY: &str = "pubkey";
 
+#[derive(Clone)]
 pub(crate) enum SignerSourceKind {
     Prompt,
     Filepath(String),
@@ -439,6 +440,8 @@ pub(crate) enum SignerSourceError {
     DerivationPathError(#[from] DerivationPathError),
     #[error(transparent)]
     IoError(#[from] std::io::Error),
+    #[error("unsupported source")]
+    UnsupportedSource,
 }
 
 pub(crate) fn parse_signer_source<S: AsRef<str>>(

+ 2 - 0
clap-v3-utils/src/nonce.rs

@@ -18,6 +18,7 @@ pub const NONCE_AUTHORITY_ARG: ArgConstant<'static> = ArgConstant {
     help: "Provide the nonce authority keypair to use when signing a nonced transaction",
 };
 
+#[allow(deprecated)]
 fn nonce_arg<'a>() -> Arg<'a> {
     Arg::new(NONCE_ARG.name)
         .long(NONCE_ARG.long)
@@ -27,6 +28,7 @@ fn nonce_arg<'a>() -> Arg<'a> {
         .help(NONCE_ARG.help)
 }
 
+#[allow(deprecated)]
 pub fn nonce_authority_arg<'a>() -> Arg<'a> {
     Arg::new(NONCE_AUTHORITY_ARG.name)
         .long(NONCE_AUTHORITY_ARG.long)

+ 1 - 0
keygen/src/keygen.rs

@@ -1,4 +1,5 @@
 #![allow(clippy::arithmetic_side_effects)]
+#![allow(deprecated)]
 use {
     bip39::{Mnemonic, MnemonicType, Seed},
     clap::{crate_description, crate_name, value_parser, Arg, ArgMatches, Command},

+ 1 - 1
remote-wallet/src/locator.rs

@@ -87,7 +87,7 @@ impl From<Infallible> for LocatorError {
     }
 }
 
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Locator {
     pub manufacturer: Manufacturer,
     pub pubkey: Option<Pubkey>,

+ 2 - 0
zk-keygen/src/main.rs

@@ -1,3 +1,5 @@
+#![allow(deprecated)]
+
 use {
     bip39::{Mnemonic, MnemonicType, Seed},
     clap::{crate_description, crate_name, Arg, ArgMatches, Command},