Skip to content

Commit 8c56be1

Browse files
johannwagnerKek5chen
authored andcommitted
feat: Started implementing OIDC in vickyctl
1 parent f34ba69 commit 8c56be1

File tree

10 files changed

+320
-51
lines changed

10 files changed

+320
-51
lines changed

Cargo.lock

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vickyctl/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ which = "6.0.1"
1515
ratatui = { version = "0.26.3", features = ["serde"] }
1616
ratatui-widgets = "0.1.6"
1717
crossterm = "0.27.0"
18+
openidconnect = "3.5.0"
19+
anyhow = "1.0.86"
20+
dirs = "5.0.1"
21+
figment = { version = "0.10.19", features = ["json", "env"] }

vickyctl/src/account.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
use openidconnect::{ErrorResponse, ClientId, IssuerUrl, core::{CoreProviderMetadata, CoreClient, CoreDeviceAuthorizationResponse, CoreAuthDisplay, CoreClientAuthMethod, CoreClaimName, CoreClaimType, CoreGrantType, CoreJweContentEncryptionAlgorithm, CoreJweKeyManagementAlgorithm, CoreJsonWebKey, CoreResponseMode, CoreResponseType, CoreSubjectIdentifierType, CoreJwsSigningAlgorithm, CoreJsonWebKeyType, CoreJsonWebKeyUse}, Scope, reqwest::{http_client}, AdditionalProviderMetadata, ProviderMetadata, DeviceAuthorizationUrl, AuthType, OAuth2TokenResponse};
2+
use serde::{Deserialize, Serialize};
3+
4+
use crate::{cli::AppContext, error::Error, FileConfig, AuthState};
5+
6+
7+
// Taken from https://github.com/ramosbugs/openidconnect-rs/blob/support/3.x/examples/okta_device_grant.rs
8+
#[derive(Clone, Debug, Deserialize, Serialize)]
9+
struct DeviceEndpointProviderMetadata {
10+
device_authorization_endpoint: DeviceAuthorizationUrl,
11+
}
12+
impl AdditionalProviderMetadata for DeviceEndpointProviderMetadata {}
13+
type DeviceProviderMetadata = ProviderMetadata<
14+
DeviceEndpointProviderMetadata,
15+
CoreAuthDisplay,
16+
CoreClientAuthMethod,
17+
CoreClaimName,
18+
CoreClaimType,
19+
CoreGrantType,
20+
CoreJweContentEncryptionAlgorithm,
21+
CoreJweKeyManagementAlgorithm,
22+
CoreJwsSigningAlgorithm,
23+
CoreJsonWebKeyType,
24+
CoreJsonWebKeyUse,
25+
CoreJsonWebKey,
26+
CoreResponseMode,
27+
CoreResponseType,
28+
CoreSubjectIdentifierType,
29+
>;
30+
31+
32+
pub fn show(auth_state: &AuthState) -> Result<(), anyhow::Error> {
33+
print!("{:?}", auth_state.clone());
34+
Ok(())
35+
}
36+
37+
pub fn login(ctx: &AppContext, vicky_url_str: String, issuer_url_str: String, client_id_str: String) -> Result<(), anyhow::Error> {
38+
39+
let client_id = ClientId::new(client_id_str.clone().to_string());
40+
let issuer_url = IssuerUrl::new(issuer_url_str.clone().to_string())?;
41+
42+
43+
let provider_metadata = DeviceProviderMetadata::discover(&issuer_url, http_client)?;
44+
45+
let device_authorization_endpoint = provider_metadata
46+
.additional_metadata()
47+
.device_authorization_endpoint
48+
.clone();
49+
50+
let client = CoreClient::from_provider_metadata(
51+
provider_metadata,
52+
client_id,
53+
None,
54+
)
55+
.set_device_authorization_uri(device_authorization_endpoint)
56+
.set_auth_type(AuthType::RequestBody);
57+
58+
let details: CoreDeviceAuthorizationResponse = client
59+
.exchange_device_code()?
60+
.add_scope(Scope::new("profile".to_string()))
61+
.request(http_client)?;
62+
63+
64+
println!("Fetching device code...");
65+
dbg!(&details);
66+
67+
// Display the URL and user-code.
68+
println!(
69+
"Open this URL in your browser:\n{}\nand enter the code: {}",
70+
details.verification_uri_complete().unwrap().secret(),
71+
details.user_code().secret()
72+
);
73+
74+
// Now poll for the token
75+
let token = client
76+
.exchange_device_access_token(&details)
77+
.request(http_client, std::thread::sleep, None)?;
78+
79+
let account_cfg = FileConfig {
80+
vicky_url: vicky_url_str,
81+
client_id: client_id_str,
82+
issuer_url: issuer_url_str,
83+
refresh_token: token.refresh_token().unwrap().secret().to_string(),
84+
};
85+
account_cfg.save()?;
86+
87+
Ok(())
88+
}
89+

vickyctl/src/cli.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,6 @@ use uuid::Uuid;
44
// TODO: Add abouts to arguments
55
#[derive(Parser, Debug, Clone)]
66
pub struct AppContext {
7-
#[clap(env)]
8-
pub vicky_url: String,
9-
10-
#[clap(env)]
11-
pub vicky_token: String,
12-
137
#[clap(long)]
148
pub humanize: bool,
159
}
@@ -38,6 +32,13 @@ pub enum TaskCommands {
3832
Finish { id: Uuid, status: String },
3933
}
4034

35+
36+
#[derive(Subcommand, Debug)]
37+
pub enum AccountCommands {
38+
Show,
39+
Login {vicky_url: String, issuer_url: String, client_id: String},
40+
}
41+
4142
#[derive(Args, Debug)]
4243
#[command(version, about = "Manage tasks on the vicky delegation server", long_about = None)]
4344
pub struct TaskArgs {
@@ -55,6 +56,16 @@ pub struct TasksArgs {
5556
pub ctx: AppContext,
5657
}
5758

59+
#[derive(Args, Debug)]
60+
#[command(version, about = "Show all accounts", long_about = None)]
61+
pub struct AccountArgs {
62+
#[command(subcommand)]
63+
pub commands: AccountCommands,
64+
65+
#[command(flatten)]
66+
pub ctx: AppContext,
67+
}
68+
5869
#[derive(Args, Debug)]
5970
#[command(version, about = "Show all poisoned locks vicky is managing", long_about = None)]
6071
pub struct LocksArgs {
@@ -80,6 +91,7 @@ pub struct ResolveArgs {
8091
#[derive(Parser, Debug)]
8192
#[command(version, about, long_about = None)]
8293
pub enum Cli {
94+
Account(AccountArgs),
8395
Task(TaskArgs),
8496
Tasks(TasksArgs),
8597
Locks(LocksArgs),

vickyctl/src/error.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ pub enum Error {
99
ReqwestDetailed(reqwest::Error, String),
1010
Io(std::io::Error),
1111
Json(serde_json::Error),
12+
Unauthenticated(),
1213
#[allow(dead_code)]
1314
Custom(&'static str),
15+
Anyhow(anyhow::Error),
1416
}
1517

1618
impl From<reqwest::Error> for Error {
@@ -37,6 +39,12 @@ impl From<serde_json::Error> for Error {
3739
}
3840
}
3941

42+
impl From<anyhow::Error> for Error {
43+
fn from(e: anyhow::Error) -> Self {
44+
Error::Anyhow(e)
45+
}
46+
}
47+
4048
impl Display for Error {
4149
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
4250
match self {
@@ -55,6 +63,8 @@ impl Display for Error {
5563
Error::Io(e) => write!(f, "Filesystem Error: {}", e),
5664
Error::Json(e) => write!(f, "Parser Error: {}", e),
5765
Error::Custom(ref str) => write!(f, "Custom Error: {}", str),
66+
Error::Anyhow(ref str) => write!(f, "Unknown Error: {}", str),
67+
Error::Unauthenticated() => write!(f, "Unauthenticated"),
5868
Error::ReqwestDetailed(e, ref detail) => {
5969
write!(f, "{}", format_http_msg(e.status(), detail))
6070
}

vickyctl/src/http_client.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,34 @@
1+
use crate::AuthState;
12
use crate::error::Error;
23
use reqwest::blocking::Client;
34
use reqwest::header::{HeaderMap, AUTHORIZATION};
45
use reqwest::StatusCode;
56
use yansi::Paint;
6-
use crate::cli::AppContext;
77

8-
pub fn prepare_client(ctx: &AppContext) -> Result<Client, Error> {
8+
pub fn prepare_client(auth_state: &AuthState) -> Result<(Client, String), Error> {
9+
10+
let base_url: String;
11+
let auth_token: String = "".to_owned();
12+
13+
match auth_state {
14+
AuthState::EnvironmentAuthenticated(envConfig) => {
15+
base_url = envConfig.url.clone();
16+
},
17+
AuthState::FileAuthenticated(fileCfg) => {
18+
base_url = fileCfg.vicky_url.clone();
19+
},
20+
AuthState::Unauthenticated => {
21+
return Err(Error::Unauthenticated())
22+
},
23+
}
24+
925
let mut default_headers = HeaderMap::new();
10-
default_headers.insert(AUTHORIZATION, ctx.vicky_token.parse().unwrap());
26+
default_headers.insert(AUTHORIZATION, auth_token.parse().unwrap());
1127
let client = Client::builder()
1228
.default_headers(default_headers)
1329
.user_agent(format!("vickyctl/{}", env!("CARGO_PKG_VERSION")))
1430
.build()?;
15-
Ok(client)
31+
Ok((client, base_url))
1632
}
1733

1834
pub fn print_http(status: Option<StatusCode>, msg: &str) {

vickyctl/src/locks/http.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::cli::AppContext;
1+
use reqwest::blocking::Client;
2+
3+
use crate::AuthState;
24
use crate::error::Error;
35
use crate::http_client::prepare_client;
46
use crate::locks::types::{LockType, PoisonedLock};
@@ -14,15 +16,15 @@ pub fn get_locks_endpoint(lock_type: LockType, detailed: bool) -> &'static str {
1416
}
1517

1618
pub fn fetch_locks_raw(
17-
ctx: &AppContext,
19+
client: &Client,
20+
vicky_url: String,
1821
lock_type: LockType,
1922
detailed: bool,
2023
) -> Result<String, Error> {
21-
let client = prepare_client(ctx)?;
2224
let request = client
2325
.get(format!(
2426
"{}/{}",
25-
ctx.vicky_url,
27+
vicky_url,
2628
get_locks_endpoint(lock_type, detailed)
2729
))
2830
.build()?;
@@ -32,18 +34,17 @@ pub fn fetch_locks_raw(
3234
Ok(locks)
3335
}
3436

35-
pub fn fetch_detailed_poisoned_locks(ctx: &AppContext) -> Result<Vec<PoisonedLock>, Error> {
36-
let raw_locks = fetch_locks_raw(ctx, LockType::Poisoned, true)?;
37+
pub fn fetch_detailed_poisoned_locks(client: &Client, vicky_url: String) -> Result<Vec<PoisonedLock>, Error> {
38+
let raw_locks = fetch_locks_raw(client, vicky_url, LockType::Poisoned, true)?;
3739
let locks: Vec<PoisonedLock> = serde_json::from_str(&raw_locks)?;
3840
Ok(locks)
3941
}
4042

41-
pub fn unlock_lock(ctx: &AppContext, lock_to_clear: &PoisonedLock) -> Result<(), Error> {
42-
let client = prepare_client(ctx)?;
43+
pub fn unlock_lock(client: &Client, vicky_url: String, lock_to_clear: &PoisonedLock) -> Result<(), Error> {
4344
let request = client
4445
.patch(format!(
4546
"{}/api/v1/locks/unlock/{}",
46-
ctx.vicky_url,
47+
vicky_url,
4748
lock_to_clear.id()
4849
))
4950
.build()?;

0 commit comments

Comments
 (0)