Skip to content

Commit

Permalink
Refactor Rust SDK for multiple autentication methods
Browse files Browse the repository at this point in the history
.

.

Rust: move auth to own module

.

.

.

.

.

Rust: restructure again

refactoring of auth module finished

add docs and update deps
  • Loading branch information
secana committed Nov 3, 2023
1 parent b337505 commit ad041c8
Show file tree
Hide file tree
Showing 18 changed files with 313 additions and 270 deletions.
12 changes: 6 additions & 6 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ websockets = "0.3.0"
serde = { version = "1.0", features = ["derive"]}
serde_json = "1.0"
thiserror = "1.0"
uuid = { version = "1.4.1", features = ["serde", "v4"] }
uuid = { version = "1.5", features = ["serde", "v4"] }
reqwest = "0.11"
regex = "1.9"
tokio = { version = "1.32", features = ["sync", "fs"] }
sha2 = "0.10.8"
regex = "1.10"
tokio = { version = "1.33", features = ["sync", "fs"] }
sha2 = "0.10"
futures = "0.3"
rand = "0.8"
async-trait = "0.1.73"
async-trait = "0.1"

[dev-dependencies]
dotenv = "0.15"
tokio = { version = "1.32", features = ["rt", "macros", "rt-multi-thread"] }
tokio = { version = "1.33", features = ["rt", "macros", "rt-multi-thread"] }
12 changes: 12 additions & 0 deletions rust/src/auth/authenticator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use crate::error::VResult;
use async_trait::async_trait;

pub static DEFAULT_TOKEN_URL: &str =
"https://account.gdata.de/realms/vaas-production/protocol/openid-connect/token";

/// This trait has to be implemented by any authentication methods for VaaS.
#[async_trait]
pub trait Authenticator {
/// Return a valid token that can be used to authenticate against the VaaS service.
async fn get_token(&self) -> VResult<String>;
}
105 changes: 105 additions & 0 deletions rust/src/auth/authenticators/client_credentials.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use crate::auth::authenticator::{Authenticator, DEFAULT_TOKEN_URL};
use crate::error::{Error, VResult};
use crate::message::OpenIdConnectTokenResponse;
use async_trait::async_trait;
use reqwest::StatusCode;
use reqwest::Url;

/// Authenticator for the VaaS service using the client credentials flow.
/// Expects a client id and a client secret.
pub struct ClientCredentials {
client_id: String,
client_secret: String,
token_url: Url,
}

impl ClientCredentials {
/// Create a new authenticator for the VaaS service using the client credentials flow.
pub fn new(client_id: String, client_secret: String) -> Self {
Self {
client_id,
client_secret,
token_url: Url::parse(DEFAULT_TOKEN_URL).unwrap(), // Safe to unwrap, as this is a constant URL and will always be valid.
}
}
/// Set the token URL to be used for authentication.
pub fn with_token_url(mut self, token_url: Url) -> Self {
self.token_url = token_url;
self
}
}

#[async_trait]
impl Authenticator for ClientCredentials {
async fn get_token(&self) -> VResult<String> {
let params = [
("client_id", self.client_id.clone()),
("client_secret", self.client_secret.clone()),
("grant_type", "client_credentials".to_string()),
];
let client = reqwest::Client::new();
let token_response = client
.post(self.token_url.clone())
.form(&params)
.send()
.await?;

match token_response.status() {
StatusCode::OK => {
let json_string = token_response.text().await?;
Ok(OpenIdConnectTokenResponse::try_from(&json_string)?.access_token)
}
status => Err(Error::FailedAuthTokenRequest(
status,
token_response.text().await.unwrap_or_default(),
)),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error::FailedAuthTokenRequest;

#[tokio::test]
async fn authenticator_returns_token() {
let token_url: Url = dotenv::var("TOKEN_URL")
.expect("No TOKEN_URL environment variable set to be used in the integration tests")
.parse()
.expect("Failed to parse TOKEN_URL environment variable");
let client_id = dotenv::var("CLIENT_ID").expect(
"No VAAS_CLIENT_ID environment variable set to be used in the integration tests",
);
let client_secret = dotenv::var("CLIENT_SECRET").expect(
"No VAAS_PASSWORD environment variable set to be used in the integration tests",
);
let authenticator =
ClientCredentials::new(client_id, client_secret).with_token_url(token_url);

let token = authenticator.get_token().await;

assert!(token.is_ok())
}

#[tokio::test]
async fn authenticator_wrong_credentials() {
let token_url: Url = dotenv::var("TOKEN_URL")
.expect("No TOKEN_URL environment variable set to be used in the integration tests")
.parse()
.expect("Failed to parse TOKEN_URL environment variable");
let client_id = "invalid".to_string();
let client_secret = "invalid".to_string();
let authenticator =
ClientCredentials::new(client_id, client_secret).with_token_url(token_url);

let token = authenticator.get_token().await;

assert!(token.is_err());
assert!(match token {
Ok(_) => false,
Err(FailedAuthTokenRequest(_, _)) => true,
_ => false,
})
}
}
9 changes: 9 additions & 0 deletions rust/src/auth/authenticators/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! # Authenticators
//!
//! This module contains the different **OAuth2 Grant Types** that can be used to authenticate against the VaaS service.
mod client_credentials;
mod password;

pub use client_credentials::ClientCredentials;
pub use password::Password;
109 changes: 109 additions & 0 deletions rust/src/auth/authenticators/password.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use crate::auth::Authenticator;
use crate::error::{Error, VResult};
use crate::message::OpenIdConnectTokenResponse;
use async_trait::async_trait;
use reqwest::{StatusCode, Url};

/// Authenticator for the VaaS service using the password flow.
/// Expects a client id, a user name and a password.
pub struct Password {
client_id: String,
user_name: String,
password: String,
token_url: Url,
}

impl Password {
/// Create a new authenticator for the VaaS service using the password flow.
pub fn new(client_id: String, user_name: String, password: String) -> Self {
Self {
client_id,
user_name,
password,
token_url: Url::parse(crate::auth::authenticator::DEFAULT_TOKEN_URL).unwrap(), // Safe to unwrap, as this is a constant URL and will always be valid.
}
}
/// Set the token URL to be used for authentication.
pub fn with_token_url(mut self, token_url: Url) -> Self {
self.token_url = token_url;
self
}
}

#[async_trait]
impl Authenticator for Password {
async fn get_token(&self) -> VResult<String> {
let params = [
("client_id", self.client_id.clone()),
("username", self.user_name.clone()),
("password", self.password.clone()),
("grant_type", "password".to_string()),
];
let client = reqwest::Client::new();
let token_response = client
.post(self.token_url.clone())
.form(&params)
.send()
.await?;

match token_response.status() {
StatusCode::OK => {
let json_string = token_response.text().await?;
Ok(OpenIdConnectTokenResponse::try_from(&json_string)?.access_token)
}
status => Err(Error::FailedAuthTokenRequest(
status,
token_response.text().await.unwrap_or_default(),
)),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error::FailedAuthTokenRequest;

#[tokio::test]
async fn authenticator_returns_token() {
let token_url: Url = dotenv::var("TOKEN_URL")
.expect("No TOKEN_URL environment variable set to be used in the integration tests")
.parse()
.expect("Failed to parse TOKEN_URL environment variable");
let client_id = dotenv::var("VAAS_CLIENT_ID").expect(
"No VAAS_CLIENT_ID environment variable set to be used in the integration tests",
);
let user_name = dotenv::var("VAAS_USER_NAME").expect(
"No VAAS_USER_NAME environment variable set to be used in the integration tests",
);
let password = dotenv::var("VAAS_PASSWORD").expect(
"No VAAS_PASSWORD environment variable set to be used in the integration tests",
);
let authenticator = Password::new(client_id, user_name, password).with_token_url(token_url);

let token = authenticator.get_token().await;

assert!(token.is_ok())
}

#[tokio::test]
async fn authenticator_wrong_credentials() {
let token_url: Url = dotenv::var("TOKEN_URL")
.expect("No TOKEN_URL environment variable set to be used in the integration tests")
.parse()
.expect("Failed to parse TOKEN_URL environment variable");
let client_id = "invalid".to_string();
let user_name = "invalid".to_string();
let password = "invalid".to_string();
let authenticator = Password::new(client_id, user_name, password).with_token_url(token_url);

let token = authenticator.get_token().await;

assert!(token.is_err());
assert!(match token {
Ok(_) => false,
Err(FailedAuthTokenRequest(_, _)) => true,
_ => false,
})
}
}
8 changes: 8 additions & 0 deletions rust/src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
//! # Authentication
//!
//! This module contains all needed funcionality to authenticate against the VaaS service.
mod authenticator;
pub mod authenticators;

pub use authenticator::Authenticator;
7 changes: 0 additions & 7 deletions rust/src/authenticator.rs

This file was deleted.

45 changes: 20 additions & 25 deletions rust/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,41 @@
//! The `Builder` struct create a new [Vaas] instance with the expected default values and allows the custom configuration.
use reqwest::Url;
use crate::auth::Authenticator;
use crate::error::VResult;
use crate::options::Options;
use crate::vaas::Vaas;
use reqwest::Url;

/// Builder struct to create a new Vaas instance with the expected default values.
/// ```rust
/// // Create a new [Vaas] instance from the builder.
/// # fn main() -> vaas::error::VResult<()> {
/// use vaas::Builder;
/// use vaas::auth::authenticators::ClientCredentials;
///
/// let authenticator = ClientCredentials::new("client_id".to_string(), "client_secret".to_string());
///
/// let vaas = Builder::new(String::from("mytoken")).build()?;
/// let vaas = Builder::new(authenticator).build()?;
/// # Ok(()) }
/// ```
pub struct Builder {
token: String,
pub struct Builder<A: Authenticator> {
authenticator: A,
url: Url,
options: Options,
}

impl Builder {
impl<A: Authenticator> Builder<A> {
/// Create a new VaasBuilder to create a [Vaas] instance.
pub fn new(token: String) -> Self {
pub fn new(authenticator: A) -> Self {
use std::str::FromStr;
Self {
token,
..Self::default()
options: Options {
keep_alive_delay_ms: 10_000,
keep_alive: true,
channel_capacity: 100,
},
authenticator,
url: Url::from_str("wss://gateway.production.vaas.gdatasecurity.de").unwrap(),
}
}

Expand Down Expand Up @@ -72,26 +82,11 @@ impl Builder {
}

/// Create a [Vaas] struct from the `VaasBuilder`.
pub fn build(self) -> VResult<Vaas> {
pub fn build(self) -> VResult<Vaas<A>> {
Ok(Vaas {
options: self.options,
token: self.token,
authenticator: self.authenticator,
url: self.url,
})
}
}

impl Default for Builder {
fn default() -> Self {
use std::str::FromStr;
Self {
options: Options {
keep_alive_delay_ms: 10_000,
keep_alive: true,
channel_capacity: 100,
},
token: String::new(),
url: Url::from_str("wss://gateway.production.vaas.gdatasecurity.de").unwrap(),
}
}
}
Empty file added rust/src/c
Empty file.
13 changes: 9 additions & 4 deletions rust/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,13 @@ impl Connection {
}

/// Request a verdict for files behind a list of URLs.
pub async fn for_url_list(&self, url_list: &[Url], ct: &CancellationToken) -> Vec<VResult<VaasVerdict>> {
let req = url_list.iter()
pub async fn for_url_list(
&self,
url_list: &[Url],
ct: &CancellationToken,
) -> Vec<VResult<VaasVerdict>> {
let req = url_list
.iter()
.map(|url| self.for_url(url, ct))
.collect::<Vec<_>>();

Expand Down Expand Up @@ -259,10 +264,10 @@ impl Connection {
match Self::parse_frame(frame) {
Ok(MessageType::VerdictResponse(vr)) => {
result_channel.send(Ok(vr))?;
},
}
Ok(MessageType::Close) => {
result_channel.send(Err(Error::ConnectionClosed))?;
},
}
Err(e) => {
result_channel.send(Err(e))?;
}
Expand Down
2 changes: 1 addition & 1 deletion rust/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ pub enum Error {
NoSessionIdInAuthResp,
/// Connection was closed, reconnect is necessary
#[error("Connection was closed")]
ConnectionClosed
ConnectionClosed,
}

impl From<PoisonError<std::sync::MutexGuard<'_, websockets::WebSocketWriteHalf>>> for Error {
Expand Down
Loading

0 comments on commit ad041c8

Please sign in to comment.