Skip to content

Commit 2d7d2d6

Browse files
committed
feat: add auth token command
Signed-off-by: Ruben Romero Montes <rromerom@redhat.com> Assisted-by: Cursor
1 parent 47dc553 commit 2d7d2d6

7 files changed

Lines changed: 114 additions & 28 deletions

File tree

README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ A command-line interface for interacting with the [Trustify](https://github.com/
44

55
## Features
66

7-
- 🔐 OAuth2 authentication (client credentials)
7+
- 🔐 OAuth2 authentication (client credentials) with token retrieval
88
- 📦 SBOM management (list, get, delete)
99
- 🔍 Duplicate detection and cleanup
10+
- ⚡ Concurrent operations with automatic retry and token refresh
1011

1112
## Installation
1213

@@ -135,6 +136,30 @@ trustify -u http://localhost:8080 \
135136

136137
---
137138

139+
### `auth token`
140+
141+
Retrieve an OAuth2 access token using the configured credentials. Useful for debugging authentication or using the token with other tools.
142+
143+
```bash
144+
trustify -u http://localhost:8080 \
145+
--sso-url http://sso.example.com/realms/trustify \
146+
--client-id my-client \
147+
--client-secret my-secret \
148+
auth token
149+
```
150+
151+
**Output:** The access token string (can be used as a Bearer token)
152+
153+
**Example with curl:**
154+
155+
```bash
156+
# Get token and use with curl
157+
TOKEN=$(trustify auth token)
158+
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/v2/sbom
159+
```
160+
161+
---
162+
138163
### `sbom get <ID>`
139164

140165
Get an SBOM by its ID. Returns the full JSON document.

src/auth.rs renamed to src/api/auth.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,42 @@ pub enum AuthError {
1414
ServerError(String),
1515
}
1616

17+
/// Authentication credentials for token refresh
18+
#[derive(Clone)]
19+
pub struct AuthCredentials {
20+
pub token_url: String,
21+
pub client_id: String,
22+
pub client_secret: String,
23+
}
24+
25+
impl AuthCredentials {
26+
/// Build credentials from SSO URL and client credentials
27+
pub fn new(sso_url: &str, client_id: &str, client_secret: &str) -> Self {
28+
let token_url = build_token_url(sso_url);
29+
Self {
30+
token_url,
31+
client_id: client_id.to_string(),
32+
client_secret: client_secret.to_string(),
33+
}
34+
}
35+
36+
/// Get a token using these credentials
37+
pub async fn get_token(&self) -> Result<String, AuthError> {
38+
get_token(&self.token_url, &self.client_id, &self.client_secret).await
39+
}
40+
}
41+
42+
/// Build the token URL from an SSO base URL
43+
pub fn build_token_url(sso_url: &str) -> String {
44+
if sso_url.ends_with("/token") {
45+
sso_url.to_string()
46+
} else if sso_url.ends_with('/') {
47+
format!("{}protocol/openid-connect/token", sso_url)
48+
} else {
49+
format!("{}/protocol/openid-connect/token", sso_url)
50+
}
51+
}
52+
1753
#[derive(Deserialize)]
1854
struct TokenResponse {
1955
access_token: String,

src/api/client.rs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use thiserror::Error;
66
use tokio::sync::RwLock;
77
use tokio::time::sleep;
88

9-
use crate::auth;
109

1110
const MAX_RETRIES: u32 = 3;
1211
const RETRY_DELAY_MS: u64 = 1000;
@@ -52,13 +51,8 @@ impl From<reqwest::Error> for ApiError {
5251
}
5352
}
5453

55-
/// Authentication credentials for token refresh
56-
#[derive(Clone)]
57-
pub struct AuthCredentials {
58-
pub token_url: String,
59-
pub client_id: String,
60-
pub client_secret: String,
61-
}
54+
// Re-export AuthCredentials from auth module
55+
pub use super::auth::AuthCredentials;
6256

6357
/// API client for Trustify with retry and token refresh support
6458
#[derive(Clone)]
@@ -111,7 +105,7 @@ impl ApiClient {
111105

112106
eprintln!("Token expired, refreshing...");
113107

114-
match auth::get_token(&creds.token_url, &creds.client_id, &creds.client_secret).await {
108+
match creds.get_token().await {
115109
Ok(new_token) => {
116110
let mut token = self.token.write().await;
117111
*token = Some(new_token);

src/api/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
pub mod client;
22
pub mod sbom;
3-
3+
pub mod auth;
44
pub use client::ApiClient;

src/commands/auth.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use clap::Subcommand;
2+
3+
use std::process;
4+
use crate::Context;
5+
use crate::api::auth::AuthCredentials;
6+
7+
#[derive(Subcommand)]
8+
pub enum AuthCommands {
9+
/// Get authentication token
10+
Token {},
11+
}
12+
13+
impl AuthCommands {
14+
pub async fn run(&self, ctx: &Context) {
15+
match self {
16+
AuthCommands::Token {} => {
17+
match ctx.config.auth_credentials() {
18+
Some((token_url, client_id, client_secret)) => {
19+
let creds = AuthCredentials::new(token_url, client_id, client_secret);
20+
match creds.get_token().await {
21+
Ok(token) => println!("{}", token),
22+
Err(e) => {
23+
eprintln!("Error: {}", e);
24+
process::exit(1);
25+
}
26+
}
27+
}
28+
None => {
29+
eprintln!("Error: SSO URL, client ID, and client secret are all required");
30+
process::exit(1);
31+
}
32+
}
33+
}
34+
}
35+
}
36+
}

src/commands/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
pub mod sbom;
2+
pub mod auth;
23

34
use clap::Subcommand;
45

56
use crate::Context;
67
pub use sbom::SbomCommands;
8+
pub use auth::AuthCommands;
79

810
#[derive(Subcommand)]
911
pub enum Commands {
@@ -12,12 +14,19 @@ pub enum Commands {
1214
#[command(subcommand)]
1315
command: SbomCommands,
1416
},
17+
18+
/// Authentication commands
19+
Auth {
20+
#[command(subcommand)]
21+
command: AuthCommands,
22+
},
1523
}
1624

1725
impl Commands {
1826
pub async fn run(&self, ctx: &Context) {
1927
match self {
2028
Commands::Sbom { command } => command.run(ctx).await,
29+
Commands::Auth { command } => command.run(ctx).await,
2130
}
2231
}
2332
}

src/main.rs

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
mod api;
2-
mod auth;
32
mod cli;
43
mod commands;
54
mod config;
@@ -8,7 +7,7 @@ use std::process;
87

98
use clap::Parser;
109

11-
use api::client::AuthCredentials;
10+
use api::auth::AuthCredentials;
1211
use api::ApiClient;
1312
use cli::Cli;
1413

@@ -28,22 +27,9 @@ async fn main() {
2827
// Build auth credentials and get initial token if configured
2928
let (token, auth_credentials) =
3029
if let Some((sso_url, client_id, client_secret)) = cli.config.auth_credentials() {
31-
let token_url = if sso_url.ends_with("/token") {
32-
sso_url.to_string()
33-
} else if sso_url.ends_with('/') {
34-
format!("{}protocol/openid-connect/token", sso_url)
35-
} else {
36-
format!("{}/protocol/openid-connect/token", sso_url)
37-
};
30+
let creds = AuthCredentials::new(sso_url, client_id, client_secret);
3831

39-
// Store credentials for token refresh
40-
let creds = AuthCredentials {
41-
token_url: token_url.clone(),
42-
client_id: client_id.to_string(),
43-
client_secret: client_secret.to_string(),
44-
};
45-
46-
match auth::get_token(&token_url, client_id, client_secret).await {
32+
match creds.get_token().await {
4733
Ok(token) => (Some(token), Some(creds)),
4834
Err(e) => {
4935
eprintln!("Error: {}", e);

0 commit comments

Comments
 (0)