Skip to content

Commit

Permalink
Create Connection abstraction for client communication
Browse files Browse the repository at this point in the history
This commit is in response to issue #199. Here, we introduce a
`Connection` struct which currently contains two things:

* `stream` -- an object representing the communication stream between
  client and service.
* `metadata` -- an optional enum instance that captures metadata about
  the connection.

This abstraction allows us to carry more information forwards toward the
frontend/authenticator/... .

Specifically, this abstraction was created with UNIX domain sockets in
mind (but the usefulness is not limited here). UNIX domain sockets allow
incoming connections to be queried for peer metadata, which is a triple
(uid, gid, pid) of the peer process that is connecting. Under certain
configurations, this can be used for authentication.

This commit places us in a position of being able to use said metadata
for authentication if needed.

Signed-off-by: Joe Ellis <[email protected]>
  • Loading branch information
Joe Ellis committed Jul 24, 2020
1 parent adaf587 commit 102139c
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 18 deletions.
17 changes: 13 additions & 4 deletions src/authenticators/direct_authenticator/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

use super::ApplicationName;
use super::Authenticate;
use crate::front::listener::ConnectionMetadata;
use log::error;
use parsec_interface::requests::request::RequestAuth;
use parsec_interface::requests::{ResponseStatus, Result};
Expand All @@ -20,7 +21,11 @@ use std::str;
pub struct DirectAuthenticator;

impl Authenticate for DirectAuthenticator {
fn authenticate(&self, auth: &RequestAuth) -> Result<ApplicationName> {
fn authenticate(
&self,
auth: &RequestAuth,
_: Option<ConnectionMetadata>,
) -> Result<ApplicationName> {
if auth.buffer.expose_secret().is_empty() {
error!("The direct authenticator does not expect empty authentication values.");
Err(ResponseStatus::AuthenticationError)
Expand All @@ -40,6 +45,7 @@ impl Authenticate for DirectAuthenticator {
mod test {
use super::super::Authenticate;
use super::DirectAuthenticator;
use crate::front::listener::ConnectionMetadata;
use parsec_interface::requests::request::RequestAuth;
use parsec_interface::requests::ResponseStatus;

Expand All @@ -49,9 +55,10 @@ mod test {

let app_name = "app_name".to_string();
let req_auth = RequestAuth::new(app_name.clone().into_bytes());
let conn_metadata = None;

let auth_name = authenticator
.authenticate(&req_auth)
.authenticate(&req_auth, conn_metadata)
.expect("Failed to authenticate");

assert_eq!(auth_name.get_name(), app_name);
Expand All @@ -60,8 +67,9 @@ mod test {
#[test]
fn failed_authentication() {
let authenticator = DirectAuthenticator {};
let conn_metadata = None;
let status = authenticator
.authenticate(&RequestAuth::new(vec![0xff; 5]))
.authenticate(&RequestAuth::new(vec![0xff; 5]), conn_metadata)
.expect_err("Authentication should have failed");

assert_eq!(status, ResponseStatus::AuthenticationError);
Expand All @@ -70,8 +78,9 @@ mod test {
#[test]
fn empty_auth() {
let authenticator = DirectAuthenticator {};
let conn_metadata = None;
let status = authenticator
.authenticate(&RequestAuth::new(Vec::new()))
.authenticate(&RequestAuth::new(Vec::new()), conn_metadata)
.expect_err("Empty auth should have failed");

assert_eq!(status, ResponseStatus::AuthenticationError);
Expand Down
12 changes: 10 additions & 2 deletions src/authenticators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

pub mod direct_authenticator;

use crate::front::listener::ConnectionMetadata;
use parsec_interface::requests::request::RequestAuth;
use parsec_interface::requests::Result;

Expand All @@ -24,12 +25,19 @@ pub struct ApplicationName(String);
///
/// Interface that must be implemented for each authentication type available for the service.
pub trait Authenticate {
/// Authenticates a `RequestAuth` payload and returns the `ApplicationName` if successfull.
/// Authenticates a `RequestAuth` payload and returns the `ApplicationName` if successful. A
/// optional `ConnectionMetadata` object is passed in too, since it is sometimes possible to
/// perform authentication based on the connection's metadata (i.e. as is the case for UNIX
/// domain sockets with peer credentials).
///
/// # Errors
///
/// If the authentification fails, returns a `ResponseStatus::AuthenticationError`.
fn authenticate(&self, auth: &RequestAuth) -> Result<ApplicationName>;
fn authenticate(
&self,
auth: &RequestAuth,
meta: Option<ConnectionMetadata>,
) -> Result<ApplicationName>;
}

impl ApplicationName {
Expand Down
4 changes: 2 additions & 2 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,10 @@ fn main() -> Result<()> {
info!("Parsec configuration reloaded.");
}

if let Some(stream) = listener.accept() {
if let Some(connection) = listener.accept() {
let front_end_handler = front_end_handler.clone();
threadpool.execute(move || {
front_end_handler.handle_request(stream);
front_end_handler.handle_request(connection);
trace!("handle_request egress");
});
} else {
Expand Down
11 changes: 8 additions & 3 deletions src/front/domain_socket.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
//! Expose Parsec functionality using Unix domain sockets as an IPC layer.
//! The local socket is created at a predefined location.
use super::listener;
use listener::Connection;
use listener::Listen;
use listener::ReadWrite;
use log::error;
use std::fs;
use std::fs::Permissions;
Expand Down Expand Up @@ -91,7 +91,7 @@ impl Listen for DomainSocketListener {
self.timeout = duration;
}

fn accept(&self) -> Option<Box<dyn ReadWrite + Send>> {
fn accept(&self) -> Option<Connection> {
let stream_result = self.listener.accept();
match stream_result {
Ok((stream, _)) => {
Expand All @@ -105,7 +105,12 @@ impl Listen for DomainSocketListener {
format_error!("Failed to set stream as blocking", err);
None
} else {
Some(Box::from(stream))
Some(Connection {
stream: Box::new(stream),
// TODO: when possible, we want to replace this with the (uid, gid, pid)
// triple for peer credentials. See listener.rs.
metadata: None,
})
}
}
Err(err) => {
Expand Down
12 changes: 6 additions & 6 deletions src/front/front_end.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
//! pass them to the rest of the service and write the responses back.
use crate::authenticators::Authenticate;
use crate::back::dispatcher::Dispatcher;
use crate::front::listener::Connection;
use derivative::Derivative;
use log::{info, trace};
use parsec_interface::requests::AuthType;
use parsec_interface::requests::ResponseStatus;
use parsec_interface::requests::{Request, Response};
use std::collections::HashMap;
use std::io::{Error, ErrorKind, Result};
use std::io::{Read, Write};

/// Read and verify request from IPC stream
///
Expand All @@ -40,17 +40,17 @@ impl FrontEndHandler {
///
/// If an error occurs during (un)marshalling, no operation will be performed and the
/// method will return.
pub fn handle_request<T: Read + Write>(&self, mut stream: T) {
pub fn handle_request(&self, mut connection: Connection) {
trace!("handle_request ingress");
// Read bytes from stream
// De-Serialise bytes into a request
let request = match Request::read_from_stream(&mut stream, self.body_len_limit) {
let request = match Request::read_from_stream(&mut connection.stream, self.body_len_limit) {
Ok(request) => request,
Err(status) => {
format_error!("Failed to read request", status);

let response = Response::from_status(status);
if let Err(status) = response.write_to_stream(&mut stream) {
if let Err(status) = response.write_to_stream(&mut connection.stream) {
format_error!("Failed to write response", status);
}
return;
Expand All @@ -63,7 +63,7 @@ impl FrontEndHandler {
// Otherwise find an authenticator that is capable to authenticate the request
} else if let Some(authenticator) = self.authenticators.get(&request.header.auth_type) {
// Authenticate the request
match authenticator.authenticate(&request.auth) {
match authenticator.authenticate(&request.auth, connection.metadata) {
// Send the request to the dispatcher
// Get a response back
Ok(app_name) => (Some(app_name), None),
Expand Down Expand Up @@ -102,7 +102,7 @@ impl FrontEndHandler {

// Serialise the response into bytes
// Write bytes to stream
match response.write_to_stream(&mut stream) {
match response.write_to_stream(&mut connection.stream) {
Ok(_) => {
if crate::utils::GlobalConfig::log_error_details() {
if let Some(app_name_string) = app_name {
Expand Down
20 changes: 19 additions & 1 deletion src/front/listener.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! The [`Listen`](https://parallaxsecond.github.io/parsec-book/parsec_service/listeners.html)
//! trait acts as an interface for the operations that must be supported by any implementation
//! of the IPC mechanism used as a Parsec front.
use derivative::Derivative;
use serde::Deserialize;
use std::time::Duration;

Expand All @@ -25,6 +26,23 @@ pub struct ListenerConfig {
pub timeout: u64,
}

/// Specifies metadata associated with a connection, if any.
#[derive(Copy, Clone, Debug)]
pub enum ConnectionMetadata {
// TODO: nothing here right now. Metadata types will be added as needed.
}

/// Represents a connection to a single client. Contains a stream, used for communication with the
/// client, and some metadata associated with the connection that might be useful elsewhere (i.e.
/// authentication, etc).
#[derive(Derivative)]
#[derivative(Debug)]
pub struct Connection {
#[derivative(Debug = "ignore")]
pub stream: Box<dyn ReadWrite + Send>,
pub metadata: Option<ConnectionMetadata>,
}

/// IPC front manager interface
///
/// Interface defining the functionality that any IPC front manager has to expose to Parsec for normal
Expand All @@ -45,5 +63,5 @@ pub trait Listen {
/// # Panics
///
/// If the listener has not been initialised before, with the `init` method.
fn accept(&self) -> Option<Box<dyn ReadWrite + Send>>;
fn accept(&self) -> Option<Connection>;
}

0 comments on commit 102139c

Please sign in to comment.