-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
rad-profile: first draft of the rad-profile CLI
Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
- Loading branch information
Showing
5 changed files
with
319 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
} |