app_store_connect/
cli.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use crate::bundle_api::{
8    BundleCapability, BundleId, BundleIdCapabilityCreateRequestDataAttributes, BundleIdPlatform,
9};
10use crate::certs_api::{self, Certificate, CertificateType};
11use crate::device_api::Device;
12use crate::profile_api::{Profile, ProfileType};
13use crate::{AppStoreConnectClient, UnifiedApiKey};
14use anyhow::Result;
15use base64::{engine::general_purpose::STANDARD as STANDARD_ENGINE, Engine};
16use clap::{Parser, Subcommand};
17use std::io::Write;
18use std::path::{Path, PathBuf};
19
20#[derive(Parser)]
21#[clap(author, version, about, long_about = None)]
22pub struct Args {
23    /// Path to unified api key.
24    #[clap(long, global = true)]
25    pub api_key: Option<PathBuf>,
26    #[clap(subcommand)]
27    pub command: Commands,
28}
29
30#[derive(Subcommand)]
31pub enum Commands {
32    /// Generates a PEM encoded RSA2048 signing key
33    GenerateSigningCertificate {
34        /// Certificate type can be one of development, distribution or notarization.
35        #[clap(long)]
36        r#type: CertificateType,
37        /// Path to write a new PEM encoded RSA2048 signing key
38        pem: PathBuf,
39    },
40    /// Creates a unified api key.
41    CreateApiKey {
42        /// Issuer id.
43        #[clap(long)]
44        issuer_id: String,
45        /// Key id.
46        #[clap(long)]
47        key_id: String,
48        /// Path to private key.
49        private_key: PathBuf,
50    },
51    Bundle {
52        #[clap(subcommand)]
53        command: BundleCommand,
54    },
55    Certificate {
56        #[clap(subcommand)]
57        command: CertificateCommand,
58    },
59    Device {
60        #[clap(subcommand)]
61        command: DeviceCommand,
62    },
63    Profile {
64        #[clap(subcommand)]
65        command: ProfileCommand,
66    },
67}
68
69impl Commands {
70    pub fn run(self, api_key: &Path) -> Result<()> {
71        match self {
72            Self::GenerateSigningCertificate { r#type, pem } => {
73                certs_api::generate_signing_certificate(api_key, r#type, &pem)?;
74            }
75            Self::CreateApiKey {
76                issuer_id,
77                key_id,
78                private_key,
79            } => {
80                UnifiedApiKey::from_ecdsa_pem_path(issuer_id, key_id, private_key)?
81                    .write_json_file(api_key)?;
82            }
83            Self::Bundle { command } => command.run(api_key)?,
84            Self::Certificate { command } => command.run(api_key)?,
85            Self::Device { command } => command.run(api_key)?,
86            Self::Profile { command } => command.run(api_key)?,
87        }
88        Ok(())
89    }
90}
91
92#[derive(Subcommand)]
93pub enum BundleCommand {
94    Register {
95        /// Bundle identifier.
96        #[clap(long)]
97        identifier: String,
98        /// Bundle name.
99        #[clap(long)]
100        name: String,
101    },
102    List,
103    Get {
104        /// Id of bundle id.
105        id: String,
106    },
107    GetProfiles {
108        /// Id of bundle id.
109        id: String,
110    },
111    GetCapabilities {
112        /// Id of bundle id.
113        id: String,
114    },
115    EnableCapability {
116        /// Id of bundle id.
117        id: String,
118        /// Capability type.
119        capability: String,
120    },
121    Delete {
122        /// Id of bundle id to revoke.
123        id: String,
124    },
125}
126
127impl BundleCommand {
128    pub fn run(self, api_key: &Path) -> Result<()> {
129        let client = AppStoreConnectClient::from_json_path(api_key)?;
130        match self {
131            Self::Register { identifier, name } => {
132                let resp = client.register_bundle_id(&identifier, &name)?;
133                print_bundle_id_header();
134                print_bundle_id(&resp.data);
135            }
136            Self::List => {
137                let resp = client.list_bundle_ids()?;
138                print_bundle_id_header();
139                for bundle_id in &resp.data {
140                    print_bundle_id(bundle_id);
141                }
142            }
143            Self::Get { id } => {
144                let resp = client.get_bundle_id(&id)?;
145                print_bundle_id_header();
146                print_bundle_id(&resp.data);
147            }
148            Self::GetCapabilities { id } => {
149                let resp = client.list_bundle_capabilities(&id)?;
150                print_capability_header();
151                for capability in resp.data {
152                    print_capability(&capability);
153                }
154            }
155            Self::EnableCapability { id, capability } => {
156                client.enable_bundle_id_capability(
157                    &id,
158                    BundleIdCapabilityCreateRequestDataAttributes {
159                        capability_type: capability.clone(),
160                    },
161                )?;
162                println!("capability {capability} enabled for bundle ID {id}");
163            }
164            Self::GetProfiles { id } => {
165                let resp = client.list_bundle_profiles(&id)?;
166                print_profile_header();
167                for profile in &resp.data {
168                    print_profile(profile);
169                }
170            }
171            Self::Delete { id } => {
172                client.delete_bundle_id(&id)?;
173            }
174        }
175        Ok(())
176    }
177}
178
179fn print_bundle_id_header() {
180    println!("{: <10} | {: <20} | {: <30}", "id", "name", "identifier");
181}
182
183fn print_bundle_id(bundle_id: &BundleId) {
184    println!(
185        "{: <10} | {: <20} | {: <30}",
186        bundle_id.id, bundle_id.attributes.name, bundle_id.attributes.identifier,
187    );
188}
189
190fn print_capability_header() {
191    println!("{: <10} | {: <20}", "id", "type");
192}
193
194fn print_capability(capability: &BundleCapability) {
195    println!(
196        "{: <10} | {: <20}",
197        capability.id, capability.attributes.capability_type
198    );
199}
200
201#[derive(Subcommand)]
202pub enum CertificateCommand {
203    Create {
204        /// Certificate type can be one of development, distribution or notarization.
205        #[clap(long)]
206        r#type: CertificateType,
207        /// Path to certificate signing request.
208        csr: PathBuf,
209    },
210    List,
211    Get {
212        /// Id of certificate.
213        id: String,
214    },
215    Revoke {
216        /// Id of certificate to revoke.
217        id: String,
218    },
219}
220
221impl CertificateCommand {
222    pub fn run(self, api_key: &Path) -> Result<()> {
223        let client = AppStoreConnectClient::from_json_path(api_key)?;
224        match self {
225            Self::Create { csr, r#type } => {
226                let csr = std::fs::read_to_string(csr)?;
227                let resp = client.create_certificate(csr, r#type)?;
228                print_certificate_header();
229                print_certificate(&resp.data);
230            }
231            Self::List => {
232                let resp = client.list_certificates()?;
233                print_certificate_header();
234                for cert in &resp.data {
235                    print_certificate(cert);
236                }
237            }
238            Self::Get { id } => {
239                let resp = client.get_certificate(&id)?;
240                let cer = pem::encode(&pem::Pem::new(
241                    "CERTIFICATE",
242                    STANDARD_ENGINE.decode(resp.data.attributes.certificate_content)?,
243                ));
244                println!("{cer}");
245            }
246            Self::Revoke { id } => {
247                client.revoke_certificate(&id)?;
248            }
249        }
250        Ok(())
251    }
252}
253
254fn print_certificate_header() {
255    println!(
256        "{: <10} | {: <50} | {: <20}",
257        "id", "name", "expiration date"
258    );
259}
260
261fn print_certificate(cert: &Certificate) {
262    let expiration_date = cert.attributes.expiration_date.split_once('T').unwrap().0;
263    println!(
264        "{: <10} | {: <50} | {: <10}",
265        cert.id, cert.attributes.name, expiration_date
266    );
267}
268
269#[derive(Subcommand)]
270pub enum DeviceCommand {
271    Register {
272        /// Name for device.
273        #[clap(long)]
274        name: String,
275        /// Platform.
276        #[clap(long)]
277        platform: BundleIdPlatform,
278        /// Unique Device Identifier
279        #[clap(long)]
280        udid: String,
281    },
282    List,
283    Get {
284        /// Id of device.
285        id: String,
286    },
287}
288
289impl DeviceCommand {
290    pub fn run(self, api_key: &Path) -> Result<()> {
291        let client = AppStoreConnectClient::from_json_path(api_key)?;
292        match self {
293            Self::Register {
294                name,
295                platform,
296                udid,
297            } => {
298                let resp = client.register_device(&name, platform, &udid)?;
299                print_device_header();
300                print_device(&resp.data);
301            }
302            Self::List => {
303                let resp = client.list_devices()?;
304                print_device_header();
305                for device in &resp.data {
306                    print_device(device);
307                }
308            }
309            Self::Get { id } => {
310                let resp = client.get_device(&id)?;
311                print_device_header();
312                print_device(&resp.data);
313            }
314        }
315        Ok(())
316    }
317}
318
319fn print_device_header() {
320    println!(
321        "{: <10} | {: <20} | {: <20} | {: <20}",
322        "id", "name", "model", "udid"
323    );
324}
325
326fn print_device(device: &Device) {
327    let model = device.attributes.model.as_deref().unwrap_or_default();
328    println!(
329        "{: <10} | {: <20} | {: <20} | {: <20}",
330        device.id, device.attributes.name, model, device.attributes.udid,
331    );
332}
333
334#[derive(Subcommand)]
335pub enum ProfileCommand {
336    Create {
337        /// Name for profile.
338        #[clap(long)]
339        name: String,
340        /// Profile type.
341        #[clap(long)]
342        profile_type: ProfileType,
343        /// Bundle identifier id.
344        #[clap(long)]
345        bundle_id: String,
346        /// Certificate ids.
347        #[clap(long)]
348        certificate: Vec<String>,
349        /// Device ids.
350        #[clap(long)]
351        device: Option<Vec<String>>,
352    },
353    List,
354    Get {
355        /// Id of profile.
356        id: String,
357    },
358    Delete {
359        /// Id of profile.
360        id: String,
361    },
362    GetBundleId {
363        /// Id of profile.
364        id: String,
365    },
366    GetCertificates {
367        /// Id of profile.
368        id: String,
369    },
370}
371
372impl ProfileCommand {
373    pub fn run(self, api_key: &Path) -> Result<()> {
374        let client = AppStoreConnectClient::from_json_path(api_key)?;
375        match self {
376            Self::Create {
377                name,
378                profile_type,
379                bundle_id,
380                certificate,
381                device,
382            } => {
383                let resp = client.create_profile(
384                    &name,
385                    profile_type,
386                    &bundle_id,
387                    &certificate,
388                    device.as_deref(),
389                )?;
390                print_profile_header();
391                print_profile(&resp.data);
392            }
393            Self::List => {
394                let resp = client.list_profiles()?;
395                print_profile_header();
396                for profile in &resp.data {
397                    print_profile(profile);
398                }
399            }
400            Self::Get { id } => {
401                let resp = client.get_profile(&id)?;
402                let profile = STANDARD_ENGINE.decode(resp.data.attributes.profile_content)?;
403                std::io::stdout().write_all(&profile)?;
404            }
405            Self::Delete { id } => {
406                client.delete_profile(&id)?;
407            }
408            Self::GetBundleId { id } => {
409                let bundle_id = client.get_profile_bundle_id(&id)?;
410                print_bundle_id_header();
411                print_bundle_id(&bundle_id.data);
412            }
413            Self::GetCertificates { id } => {
414                let resp = client.list_profile_certificates(&id)?;
415                print_certificate_header();
416                for cert in &resp.data {
417                    print_certificate(cert);
418                }
419            }
420        }
421        Ok(())
422    }
423}
424
425fn print_profile_header() {
426    println!(
427        "{: <10} | {: <20} | {: <20} | {: <20}",
428        "id", "name", "type", "expiration date"
429    );
430}
431
432fn print_profile(profile: &Profile) {
433    let expiration_date = profile
434        .attributes
435        .expiration_date
436        .split_once('T')
437        .unwrap()
438        .0;
439    println!(
440        "{: <10} | {: <20} | {: <20} | {: <20}",
441        profile.id, profile.attributes.name, profile.attributes.profile_type, expiration_date,
442    );
443}