Skip to content

Commit

Permalink
rad-profile: first draft of the rad-profile CLI
Browse files Browse the repository at this point in the history
Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
  • Loading branch information
FintanH committed Jul 27, 2021
1 parent fb03315 commit 00e5d03
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 0 deletions.
22 changes: 22 additions & 0 deletions profile/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "radicle-profile"
version = "0.1.0"
authors = ["The Radicle Team <dev@radicle.xyz>"]
edition = "2018"
license = "GPL-3.0-or-later"

[lib]
doctest = true
test = false

[dependencies]
anyhow = "1"
argh = "0"
thiserror = "1"
serde = "1"

[dependencies.librad]
path = "../librad"

[dependencies.radicle-keystore]
version = "0.1"
7 changes: 7 additions & 0 deletions profile/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright © 2021 The Radicle Link Contributors
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.

pub mod args;
pub mod main;
82 changes: 82 additions & 0 deletions profile/src/cli/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright © 2021 The Radicle Link Contributors
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.

use argh::FromArgs;

use librad::profile::ProfileId;

/// Management of Radicle profiles and their associated configuration data.
#[derive(Debug, FromArgs)]
pub struct Args {
#[argh(subcommand)]
pub command: Command,
}

#[derive(Debug, FromArgs)]
#[argh(subcommand)]
pub enum Command {
Create(Create),
Get(Get),
Set(Set),
List(List),
Peer(GetPeerId),
Paths(GetPaths),
Ssh(SshAdd),
}

/// Create a new profile, generating a new secret key and initialising
/// configurations and storage.
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "create")]
pub struct Create {}

/// Get the currently active profile.
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "get")]
pub struct Get {}

/// Set the active profile.
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "set")]
pub struct Set {
/// the identifier to set the active profile to
#[argh(option)]
pub id: ProfileId,
}

/// List all profiles that have been created
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "list")]
pub struct List {}

/// Get the peer identifier associated with the provided profile identfier. If
/// no profile was provided, then the active one is used.
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "peer-id")]
pub struct GetPeerId {
/// the identifier to look up
#[argh(option)]
pub id: Option<ProfileId>,
}

/// Get the paths associated with the provided profile identfier. If no profile
/// was provided, then the active one is used.
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "paths")]
pub struct GetPaths {
/// the identifier to look up
#[argh(option)]
pub id: Option<ProfileId>,
}

/// Add the profile's associated secrety key to the ssh-agent. If no profile was
/// provided, then the active one is used.
#[derive(Debug, FromArgs)]
#[argh(subcommand, name = "ssh-add")]
pub struct SshAdd {
/// the identifier to look up
#[argh(option)]
pub id: Option<ProfileId>,
}
71 changes: 71 additions & 0 deletions profile/src/cli/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright © 2021 The Radicle Link Contributors
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.

use radicle_keystore::{
crypto::{KdfParams, Pwhash},
pinentry::Prompt,
};

use crate::{create, get, list, paths, peer_id, set, ssh_add};

use super::args::*;

pub fn main() -> anyhow::Result<()> {
let Args { command } = argh::from_env();
eval(command)
}

fn crypto() -> Pwhash<Prompt<'static>> {
let prompt = Prompt::new("please enter your passphrase: ");
Pwhash::new(prompt, KdfParams::recommended())
}

fn eval(command: Command) -> anyhow::Result<()> {
match command {
Command::Create(Create {}) => {
let (profile, peer_id) = create(crypto())?;
println!("profile id: {}", profile.id());
println!("peer id: {}", peer_id);
},
Command::Get(Get {}) => {
let profile = get()?;
match profile {
Some(profile) => println!("{}", profile.id()),
None => println!(
"no active profile found, perhaps you want to run `rad profile create`?"
),
}
},
Command::Set(Set { id }) => {
set(id.clone())?;
println!("successfully set active profile id to {}", id);
},
Command::List(List {}) => {
let profiles = list()?;
for profile in profiles {
println!("{}", profile.id());
}
},
Command::Peer(GetPeerId { id }) => {
let peer_id = peer_id(id)?;
println!("{}", peer_id);
},
Command::Paths(GetPaths { id }) => {
let paths = paths(id)?;
println!("git: {}", paths.git_dir().display());
println!("git includes: {}", paths.git_includes_dir().display());
println!("keys: {}", paths.keys_dir().display());
},
Command::Ssh(SshAdd { id }) => {
let (id, peer_id) = ssh_add(id, crypto())?;
println!(
"added key for profile id `{}` and peer id `{}`",
id, peer_id
);
},
}

Ok(())
}
137 changes: 137 additions & 0 deletions profile/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright © 2021 The Radicle Link Contributors
//
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
// Linking Exception. For full terms see the included LICENSE file.

use std::{error, fmt};

use serde::{de::DeserializeOwned, Serialize};
use thiserror::Error;

use librad::{
git::storage::{self, read, ReadOnly, Storage},
keys::{IntoSecretKeyError, PublicKey, SecretKey},
paths::Paths,
peer::PeerId,
profile::{self, Profile, ProfileId, RadHome},
};
use radicle_keystore::{crypto::Crypto, file, FileStorage, Keystore as _};

pub mod cli;

const KEY_FILE: &str = "librad.key";

#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error(transparent)]
Keystore(Box<dyn error::Error + Send + Sync + 'static>),
#[error("no active profile was found, perhaps you need to create one")]
NoActiveProfile,
#[error("no profile was found for `{0}`")]
NoProfile(ProfileId),
#[error(transparent)]
Profile(#[from] profile::Error),
#[error(transparent)]
Storage(#[from] storage::error::Init),
#[error(transparent)]
ReadOnly(#[from] read::error::Init),
}

impl<C> From<file::Error<C, IntoSecretKeyError>> for Error
where
C: fmt::Debug + fmt::Display + Send + Sync + 'static,
{
fn from(err: file::Error<C, IntoSecretKeyError>) -> Self {
Self::Keystore(Box::new(err))
}
}

fn file_storage<C>(profile: &Profile, crypto: C) -> FileStorage<C, PublicKey, SecretKey, ()>
where
C: Crypto,
{
FileStorage::new(&profile.paths().keys_dir().join(KEY_FILE), crypto)
}

fn get_or_active<P>(home: &RadHome, id: P) -> Result<Profile, Error>
where
P: Into<Option<ProfileId>>,
{
match id.into() {
Some(id) => Profile::get(&home, id.clone())?.ok_or_else(|| Error::NoProfile(id)),
None => Profile::active(&home)?.ok_or(Error::NoActiveProfile),
}
}

/// Initialise a [`Profile`], generating a new [`SecretKey`] and [`Storage`].
pub fn create<C: Crypto>(crypto: C) -> Result<(Profile, PeerId), Error>
where
C::Error: fmt::Debug + fmt::Display + Send + Sync + 'static,
C::SecretBox: Serialize + DeserializeOwned,
{
let home = RadHome::new();
let profile = Profile::new(&home)?;
Profile::set(&home, profile.id().clone())?;
let key = SecretKey::new();
let mut store: FileStorage<C, PublicKey, SecretKey, _> = file_storage(&profile, crypto);
store.put_key(key.clone())?;
Storage::open(profile.paths(), key.clone())?;

Ok((profile, PeerId::from(key)))
}

/// Get the current active `ProfileId`.
pub fn get() -> Result<Option<Profile>, Error> {
let home = RadHome::new();
Profile::active(&home).map_err(Error::from)
}

/// Set the active profile to the given `ProfileId`.
pub fn set(id: ProfileId) -> Result<(), Error> {
let home = RadHome::new();
Profile::set(&home, id).map_err(Error::from).map(|_| ())
}

/// List the set of active profiles that exist.
pub fn list() -> Result<Vec<Profile>, Error> {
let home = RadHome::new();
Profile::list(&home).map_err(Error::from)
}

/// Get the `PeerId` associated to the given [`ProfileId`]
pub fn peer_id<P>(id: P) -> Result<PeerId, Error>
where
P: Into<Option<ProfileId>>,
{
let home = RadHome::new();
let profile = get_or_active(&home, id)?;
let read = ReadOnly::open(profile.paths())?;
Ok(*read.peer_id())
}

pub fn paths<P>(id: P) -> Result<Paths, Error>
where
P: Into<Option<ProfileId>>,
{
let home = RadHome::new();
get_or_active(&home, id).map(|p| p.paths().clone())
}

/// Add a profile's [`SecretKey`] to the `ssh-agent`.
pub fn ssh_add<P, C>(id: P, crypto: C) -> Result<(ProfileId, PeerId), Error>
where
C: Crypto,
C::Error: fmt::Debug + fmt::Display + Send + Sync + 'static,
C::SecretBox: Serialize + DeserializeOwned,
P: Into<Option<ProfileId>>,
{
let home = RadHome::new();
let profile = get_or_active(&home, id)?;
let store = file_storage(&profile, crypto);
let key = store.get_key()?;
let peer_id = PeerId::from(key.public_key);
println!("TODO: {}", peer_id);

Ok((profile.id().clone(), peer_id))
}

0 comments on commit 00e5d03

Please sign in to comment.