Skip to content

Commit

Permalink
Merge pull request #5 from Lachstec/feature/password_auth
Browse files Browse the repository at this point in the history
Implement authentication via Username and Password closing #3
  • Loading branch information
Lachstec authored Feb 14, 2024
2 parents cafebab + ae7059b commit b725443
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 47 deletions.
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description = """
An asynchrounous gNMI client to interact with and manage network devices.
"""
name = "ginmi"
version = "0.1.0"
version = "0.1.1"
edition = "2021"
keywords = ["grpc", "async", "gnmi", "network-automation"]
license = "MIT OR Apache-2.0"
Expand All @@ -22,6 +22,10 @@ tokio = { version = "1.35.1", features = ["rt-multi-thread", "macros"] }
prost = "0.12.3"
tonic = { version = "0.11.0", features = ["transport", "tls", "tls-roots"] }
thiserror = "1.0.56"
tower-service = "0.3.2"
# Needs to match tonics version of http, else implementations of the Service trait break.
http = "0.2.0"
tower = "0.4.13"

[build-dependencies]
tonic-build = "0.11.0"
17 changes: 10 additions & 7 deletions build.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
fn main() {
let proto_dir = "proto";
println!("cargo:rerun-if-changed={}", proto_dir);

tonic_build::configure()
.build_server(false)
.compile_well_known_types(true)
.compile(
&["proto/gnmi/gnmi.proto",
&[
"proto/gnmi/gnmi.proto",
"proto/gnmi_ext/gnmi_ext.proto",
"proto/target/target.proto",
"proto/collector/collector.proto",
"proto/google.proto"
"proto/google.proto",
],
&["proto"]
)?;
Ok(())
}
&[proto_dir],
).expect("Failed to compile protobuf files");
}
64 changes: 64 additions & 0 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use http::{HeaderValue, Request};
use std::error::Error;
use std::sync::Arc;
use std::task::{Context, Poll};
use tonic::codegen::Body;
use tower_service::Service;

/// Service that injects username and password into the request metadata
#[derive(Debug, Clone)]
pub struct AuthService<S> {
inner: S,
username: Option<Arc<HeaderValue>>,
password: Option<Arc<HeaderValue>>,
}

impl<S> AuthService<S> {
#[inline]
pub fn new(
inner: S,
username: Option<Arc<HeaderValue>>,
password: Option<Arc<HeaderValue>>,
) -> Self {
Self {
inner,
username,
password,
}
}
}

/// Implementation of Service so that it plays nicely with tonic.
/// Trait bounds have to match those specified on [`tonic::client::GrpcService`]
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for AuthService<S>
where
S: Service<Request<ReqBody>, Response = ResBody>,
S::Error:,
ResBody: Body,
<ResBody as Body>::Error: Into<Box<dyn Error + Send + Sync>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;

#[inline]
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

#[inline]
fn call(&mut self, mut request: Request<ReqBody>) -> Self::Future {
if let Some(user) = &self.username {
if let Some(pass) = &self.password {
request
.headers_mut()
.insert("username", user.as_ref().clone());
request
.headers_mut()
.insert("password", pass.as_ref().clone());
}
}

self.inner.call(request)
}
}
62 changes: 33 additions & 29 deletions src/client/client.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use std::str::FromStr;
use tonic::transport::{Uri, ClientTlsConfig, Certificate, Channel};
use crate::auth::AuthService;
use crate::error::GinmiError;
use crate::gen::gnmi::{CapabilityRequest, CapabilityResponse};
use crate::gen::gnmi::g_nmi_client::GNmiClient;

type ClientConn = GNmiClient<Channel>;
use crate::gen::gnmi::{CapabilityRequest, CapabilityResponse};
use http::HeaderValue;
use std::str::FromStr;
use std::sync::Arc;
use tonic::transport::{Certificate, Channel, ClientTlsConfig, Uri};

/// Provides the main functionality of connection to a target device
/// and manipulating configuration or querying telemetry.
#[derive(Debug, Clone)]
pub struct Client(ClientConn);
pub struct Client {
inner: GNmiClient<AuthService<Channel>>,
}

impl<'a> Client {
/// Create a [`ClientBuilder`] that can create [`Client`]s.
Expand All @@ -35,17 +38,15 @@ impl<'a> Client {
/// ```
pub async fn capabilities(&mut self) -> CapabilityResponse {
let req = CapabilityRequest::default();
match self.0.capabilities(req).await {
Ok(val) => {
val.into_inner()
},
Err(e) => panic!("Error getting capabilities: {:?}", e)
match self.inner.capabilities(req).await {
Ok(val) => val.into_inner(),
Err(e) => panic!("Error getting capabilities: {:?}", e),
}
}
}

#[derive(Debug, Copy, Clone)]
struct Credentials<'a> {
pub struct Credentials<'a> {
username: &'a str,
password: &'a str,
}
Expand All @@ -71,10 +72,7 @@ impl<'a> ClientBuilder<'a> {

/// Configure credentials to use for connecting to the target device.
pub fn credentials(mut self, username: &'a str, password: &'a str) -> Self {
self.creds = Some(Credentials {
username,
password
});
self.creds = Some(Credentials { username, password });
self
}

Expand All @@ -95,12 +93,10 @@ impl<'a> ClientBuilder<'a> {
/// - Returns [`GinmiError::TransportError`] if the TLS-Settings are invalid.
/// - Returns [`GinmiError::TransportError`] if a connection to the target could not be
/// established.
pub async fn build(self) -> Result<Client, GinmiError>{
pub async fn build(self) -> Result<Client, GinmiError> {
let uri = match Uri::from_str(self.target) {
Ok(u) => u,
Err(e) => {
return Err(GinmiError::InvalidUriError(e.to_string()))
}
Err(e) => return Err(GinmiError::InvalidUriError(e.to_string())),
};

let mut endpoint = Channel::builder(uri);
Expand All @@ -109,13 +105,23 @@ impl<'a> ClientBuilder<'a> {
endpoint = endpoint.tls_config(self.tls_settings.unwrap())?;
}

if self.creds.is_some() {
todo!("passing credentials is currently not implemented")
}

let channel = endpoint.connect().await?;

Ok(Client(GNmiClient::new(channel)))
return if let Some(creds) = self.creds {
let user_header = HeaderValue::from_str(creds.username)?;
let pass_header = HeaderValue::from_str(creds.password)?;
Ok(Client {
inner: GNmiClient::new(AuthService::new(
channel,
Some(Arc::new(user_header)),
Some(Arc::new(pass_header)),
)),
})
} else {
Ok(Client {
inner: GNmiClient::new(AuthService::new(channel, None, None)),
})
};
}
}

Expand All @@ -125,9 +131,7 @@ mod tests {

#[tokio::test]
async fn invalid_uri() {
let client = Client::builder("$$$$")
.build()
.await;
let client = Client::builder("$$$$").build().await;
assert!(client.is_err());
}

Expand All @@ -139,4 +143,4 @@ mod tests {
.await;
assert!(client.is_err());
}
}
}
5 changes: 1 addition & 4 deletions src/client/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
mod client;

pub use client::{
Client,
ClientBuilder
};
pub use client::{Client, ClientBuilder};
4 changes: 3 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ pub enum GinmiError {
TransportError(#[from] tonic::transport::Error),
#[error("invalid uri passed as target: {}", .0)]
InvalidUriError(String),
}
#[error("invalid header in grpc request: {}", .0)]
InvalidHeaderValue(#[from] http::header::InvalidHeaderValue),
}
8 changes: 3 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
//!
//! Provides a Client to modify and retrieve configuration from target network devices,
//! as well as various telemetry data.
mod auth;
mod client;
mod error;

pub use client::{
Client,
ClientBuilder,
};
pub use client::{Client, ClientBuilder};

pub use error::GinmiError;

Expand All @@ -30,4 +28,4 @@ pub(crate) mod gen {
tonic::include_proto!("google.protobuf");
}
}
}
}

0 comments on commit b725443

Please sign in to comment.