1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use crate::commands::config::{keyring_backend_help, keyring_backend_parser};
use anyhow::{bail, Context, Result};
use clap::Args;
use dialoguer::{theme::ColorfulTheme, Confirm, Password};
use p256::ecdsa::SigningKey;
use rand_core::OsRng;
use warg_client::keyring::Keyring;
use warg_client::{Config, RegistryUrl};

use super::CommonOptions;

/// Manage auth tokens for interacting with a registry.
#[derive(Args)]
pub struct LoginCommand {
    /// The common command options.
    #[clap(flatten)]
    pub common: CommonOptions,

    /// The URL of the registry to use.
    #[clap(value_name = "URL")]
    #[arg(hide = true)]
    pub registry_url: Option<String>,

    /// Ignore federation hints.
    #[clap(long)]
    pub ignore_federation_hints: bool,

    /// Auto accept federation hints.
    #[clap(long)]
    pub auto_accept_federation_hints: bool,

    /// The backend to use for keyring access
    #[clap(long, value_name = "KEYRING_BACKEND", value_parser = keyring_backend_parser, long_help = keyring_backend_help())]
    pub keyring_backend: Option<String>,
}

impl LoginCommand {
    /// Executes the command.
    pub async fn exec(mut self) -> Result<()> {
        if self.registry_url.is_some() {
            if self.common.registry.is_some() {
                bail!("Registry URL provided in two different arguments. Use only one.");
            }
            self.common.registry = self.registry_url;
        }
        let mut config = self.common.read_config()?;
        let mut registry_url = &self
            .common
            .registry
            .as_ref()
            .map(RegistryUrl::new)
            .transpose()?
            .map(|u| u.to_string());
        config.ignore_federation_hints = self.ignore_federation_hints;
        config.auto_accept_federation_hints = self.auto_accept_federation_hints;

        // set keyring backend, if specified
        if self.keyring_backend.is_some() {
            config.keyring_backend = self.keyring_backend;
        }

        if registry_url.is_none() && config.home_url.is_none() {
            bail!("Please set your registry: warg login --registry <registry-url>");
        }

        let mut changing_home_registry = false;

        if registry_url.is_some()
            && registry_url != &config.home_url
            && Confirm::with_theme(&ColorfulTheme::default())
                .with_prompt(format!(
                    "Set `{registry}` as your home (or default) registry?",
                    registry = registry_url.as_deref().unwrap(),
                ))
                .default(true)
                .interact()?
        {
            config.home_url.clone_from(registry_url);
            config.write_to_file(&Config::default_config_path()?)?;

            // reset if changing home registry
            changing_home_registry = true;
        } else if registry_url.is_none() {
            registry_url = &config.home_url;
        }

        let keyring = Keyring::from_config(&config)?;
        config.keyring_auth = true;

        let client = if *registry_url == config.home_url {
            self.common.create_client(&config).await?
        } else {
            let mut config = config.clone();
            config.home_url.clone_from(registry_url);
            self.common.create_client(&config).await?
        };

        // the client may resolve the registry to well-known on a different registry host,
        // so replace the `registry_url` with that host
        let registry_url = Some(client.url().to_string());

        if changing_home_registry {
            client.reset_namespaces().await?;
            client.reset_registry().await?;
        }

        let prompt = format!(
            "Enter auth token for registry: {registry}",
            registry = client.url().registry_domain(),
        );

        if config.keys.is_empty() {
            config.keys.insert("default".to_string());
            let key = SigningKey::random(&mut OsRng).into();
            keyring.set_signing_key(None, &key, &mut config.keys, registry_url.as_deref())?;
            let public_key = key.public_key();
            let token = Password::with_theme(&ColorfulTheme::default())
                .with_prompt(prompt)
                .interact()
                .context("failed to read token")?;
            keyring.set_auth_token(&RegistryUrl::new(registry_url.as_deref().unwrap())?, &token)?;
            config.write_to_file(&Config::default_config_path()?)?;
            println!("Auth token was set successfully, and generated default key.");
            println!("Public Key: {public_key}");
            return Ok(());
        }

        let token = Password::with_theme(&ColorfulTheme::default())
            .with_prompt(prompt)
            .interact()
            .context("failed to read token")?;
        keyring.set_auth_token(&RegistryUrl::new(registry_url.as_deref().unwrap())?, &token)?;
        config.write_to_file(&Config::default_config_path()?)?;
        println!("Auth token was set successfully.");

        if let Ok(private_key) = keyring.get_signing_key(
            self.common.registry.as_deref(),
            &config.keys,
            registry_url.as_deref(),
        ) {
            println!("\nSigning key is still available:");
            let public_key = private_key.public_key();
            println!("Key ID: {}", public_key.fingerprint());
            println!("Public Key: {public_key}");
        }

        Ok(())
    }
}