use std::collections::HashSet;
use tokio::sync::broadcast;
use tracing::{debug, error};
use crate::oauth2::OAuth2Credentials;
type Result<T> = core::result::Result<T, async_nats::Error>;
pub const DEFAULT_U_OS_NATS_ADDRESS: &str = "nats://127.0.0.1:49360";
pub const DEFAULT_U_OS_OAUTH2_ENDPOINT: &str = "https://127.0.0.1/oauth2/token";
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum NatsPermission {
VariableHubRead,
VariableHubReadWrite,
VariableHubProvide,
}
impl NatsPermission {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
NatsPermission::VariableHubRead => "hub.variables.readonly",
NatsPermission::VariableHubReadWrite => "hub.variables.readwrite",
NatsPermission::VariableHubProvide => "hub.variables.provide",
}
}
}
pub type NatsPermissionList = HashSet<String>;
#[derive(Clone, Debug)]
pub struct AuthenticationSettings {
pub permissions: NatsPermissionList,
pub oauth2_endpoint: String,
pub creds: Option<OAuth2Credentials>,
}
pub struct AuthenticationSettingsBuilder {
settings: AuthenticationSettings,
}
impl AuthenticationSettingsBuilder {
#[must_use]
pub fn new(permission: NatsPermission) -> Self {
Self {
settings: AuthenticationSettings {
permissions: NatsPermissionList::from([permission.as_str().to_owned()]),
oauth2_endpoint: DEFAULT_U_OS_OAUTH2_ENDPOINT.to_string(),
creds: None,
},
}
}
#[must_use]
pub fn add_permission(mut self, permission: NatsPermission) -> Self {
self.settings
.permissions
.insert(permission.as_str().to_owned());
self
}
#[must_use]
pub fn with_credentials(mut self, creds: OAuth2Credentials) -> Self {
self.settings.creds = Some(creds);
self
}
#[must_use]
pub fn with_custom_oauth2_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.settings.oauth2_endpoint = endpoint.into();
self
}
#[must_use]
pub fn build(self) -> AuthenticationSettings {
self.settings
}
}
#[derive(Debug)]
pub enum NatsAuthenticationMethod {
Unauthenticated,
#[allow(missing_docs)]
UsernameAndPassword { username: String, password: String },
Token(String),
OAuth2Client(AuthenticationSettings),
}
#[derive(Debug)]
pub struct AuthenticatedNatsConnection {
nats_client: async_nats::Client,
event_sender: broadcast::Sender<async_nats::Event>,
nats_permissions: Option<NatsPermissionList>,
client_name: String,
}
impl AuthenticatedNatsConnection {
pub async fn new(
nats_server_addr: impl Into<String>,
auth_settings: &AuthenticationSettings,
) -> Result<Self> {
let client_name = auth_settings
.creds
.as_ref()
.map(|creds| creds.client_name.clone());
Self::connect_with_auth_method(
nats_server_addr,
client_name,
NatsAuthenticationMethod::OAuth2Client(auth_settings.clone()),
true,
)
.await
}
pub async fn connect_with_auth_method(
nats_server_addr: impl Into<String>,
client_name: Option<impl Into<String>>,
auth_method: NatsAuthenticationMethod,
wait_for_con: bool,
) -> Result<Self> {
let mut client_name =
client_name.map_or_else(|| "_UNAUTHENTICATED".to_string(), Into::into);
if let NatsAuthenticationMethod::Unauthenticated = auth_method {
client_name = "_UNAUTHENTICATED".to_string();
}
let (event_sender, _) = broadcast::channel(128);
let event_receiver = event_sender.subscribe();
let nats_client = Self::connect_to_nats(
&auth_method,
nats_server_addr.into(),
&client_name,
event_sender.clone(),
)
.await?;
let nats_permissions =
if let NatsAuthenticationMethod::OAuth2Client(auth_settings) = auth_method {
Some(auth_settings.permissions)
} else {
None
};
let instance = Self {
nats_client: nats_client.clone(),
event_sender,
nats_permissions,
client_name,
};
if wait_for_con {
Self::wait_for_connection(event_receiver).await;
}
Ok(instance)
}
async fn wait_for_connection(mut event_receiver: broadcast::Receiver<async_nats::Event>) {
while let Ok(event) = event_receiver.recv().await {
if let async_nats::Event::Connected = event {
break;
}
}
}
#[must_use]
pub fn get_client_name(&self) -> &str {
&self.client_name
}
#[must_use]
pub fn get_client(&self) -> &async_nats::Client {
&self.nats_client
}
#[must_use]
pub fn get_events(&self) -> broadcast::Receiver<async_nats::Event> {
self.event_sender.subscribe()
}
#[must_use]
pub fn get_permissions(&self) -> &Option<NatsPermissionList> {
&self.nats_permissions
}
fn setup_nats_auth(auth_method: &NatsAuthenticationMethod) -> async_nats::ConnectOptions {
match auth_method {
NatsAuthenticationMethod::Unauthenticated => async_nats::ConnectOptions::new(),
NatsAuthenticationMethod::UsernameAndPassword { username, password } => {
async_nats::ConnectOptions::new()
.user_and_password(username.clone(), password.clone())
.retry_on_initial_connect()
}
NatsAuthenticationMethod::Token(token) => {
async_nats::ConnectOptions::new().token(token.clone())
}
NatsAuthenticationMethod::OAuth2Client(auth_settings) => {
Self::setup_oauth2_client_auth(auth_settings)
}
}
}
fn setup_oauth2_client_auth(
auth_settings: &AuthenticationSettings,
) -> async_nats::ConnectOptions {
let token_endpoint = auth_settings.oauth2_endpoint.clone();
let scope_list = auth_settings
.permissions
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(" ");
if let Some(creds) = auth_settings.creds.clone() {
if creds.client_id.is_empty() {
return async_nats::ConnectOptions::new();
}
async_nats::ConnectOptions::with_auth_callback(move |_| {
debug!("Requesting token for client id: {}", creds.client_id);
let creds = creds.clone();
let token_endpoint = token_endpoint.clone();
let scope_list = scope_list.clone();
async move {
let result = creds.request_token(&token_endpoint, &scope_list).await;
match result {
Ok(token_response) => {
let mut auth = async_nats::Auth::new();
auth.token = Some(token_response.access_token);
Ok(auth)
}
Err(e) => {
let error_text = format!("Error requesting token: {e}");
error!(error_text);
Err(async_nats::AuthError::new(error_text))
}
}
}
})
.retry_on_initial_connect()
} else {
async_nats::ConnectOptions::new()
}
}
async fn connect_to_nats(
auth_method: &NatsAuthenticationMethod,
nats_hostname: String,
client_name: &str,
event_sender: broadcast::Sender<async_nats::Event>,
) -> Result<async_nats::Client> {
let connection_options = Self::setup_nats_auth(auth_method);
let connection_options = connection_options
.name(client_name)
.custom_inbox_prefix(format!("_INBOX.{client_name}"));
let connection_options = connection_options
.event_callback(move |event| {
let event_sender = event_sender.clone();
async move {
event_sender.send(event).ok();
}
})
.reconnect_delay_callback(|attempts| {
let duration_sec = match attempts {
1 => 0,
2..=10 => 5,
11..=20 => 30,
_ => 300,
};
debug!(
"Reconnect in {}s, current attempt: {attempts}",
duration_sec,
);
std::time::Duration::from_secs(duration_sec)
});
let client = connection_options.connect(nats_hostname).await?;
Ok(client)
}
}