use anyhow::{Context, Result};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use parking_lot::RwLock;
use rand::RngCore;
use rivet_error::RivetError as RivetErrorDerive;
use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
use subtle::ConstantTimeEq;
use crate::ActorContext;
use crate::actor::internal_storage;
use crate::actor::keys::INSPECTOR_TOKEN_KEY;
const INSPECTOR_TOKEN_ENV: &str = "_RIVET_TEST_INSPECTOR_TOKEN";
const INSPECTOR_TOKEN_BYTES: usize = 32;
static INSPECTOR_TEST_TOKEN_OVERRIDE: OnceLock<RwLock<Option<String>>> = OnceLock::new();
pub fn set_test_inspector_token_override(token: Option<String>) {
*INSPECTOR_TEST_TOKEN_OVERRIDE
.get_or_init(|| RwLock::new(None))
.write() = token.filter(|token| !token.is_empty());
}
#[derive(Clone, Copy, Debug, Default)]
pub struct InspectorAuth;
#[derive(RivetErrorDerive, Clone, Debug, Deserialize, Serialize)]
#[error(
"inspector",
"unauthorized",
"Inspector request requires a valid bearer token"
)]
struct InspectorUnauthorized;
impl InspectorAuth {
pub fn new() -> Self {
Self
}
pub async fn verify(&self, ctx: &ActorContext, bearer_token: Option<&str>) -> Result<()> {
let Some(bearer_token) = bearer_token.filter(|token| !token.is_empty()) else {
return Err(InspectorUnauthorized.build());
};
if let Some(configured_token) = configured_test_token() {
return verify_token_bytes(bearer_token.as_bytes(), configured_token.as_bytes());
}
let stored_token = internal_storage::load_inspector_token(ctx.sql())
.await
.ok()
.flatten()
.map(String::into_bytes)
.ok_or_else(|| InspectorUnauthorized.build())?;
verify_token_bytes(bearer_token.as_bytes(), &stored_token)
}
}
pub async fn init_inspector_token(ctx: &ActorContext) -> Result<()> {
if configured_test_token().is_some() {
return Ok(());
}
if let Some(token) = internal_storage::load_inspector_token(ctx.sql())
.await
.context("load inspector token from sqlite")?
{
return ensure_inspector_token_mirror(ctx, &token).await;
}
if let Some(token) = new_inspector_token() {
let (sqlite, mirror) = futures::join!(
internal_storage::persist_inspector_token(ctx.sql(), &token),
persist_inspector_token_mirror(ctx, &token),
);
sqlite.context("persist inspector token to sqlite")?;
mirror?;
tracing::debug!(actor_id = %ctx.actor_id(), "generated new inspector token");
}
Ok(())
}
pub(crate) async fn ensure_inspector_token_mirror(ctx: &ActorContext, token: &str) -> Result<()> {
if configured_test_token().is_some() {
return Ok(());
}
let mirror = ctx
.legacy_kv()
.batch_get(&[&INSPECTOR_TOKEN_KEY])
.await?
.pop()
.flatten();
if mirror.as_deref() != Some(token.as_bytes()) {
persist_inspector_token_mirror(ctx, token).await?;
}
Ok(())
}
pub(crate) fn new_inspector_token() -> Option<String> {
configured_test_token()
.is_none()
.then(generate_inspector_token)
}
pub(crate) async fn persist_inspector_token_mirror(ctx: &ActorContext, token: &str) -> Result<()> {
ctx.legacy_kv()
.put(&INSPECTOR_TOKEN_KEY, token.as_bytes())
.await
.context("persist inspector token mirror to kv")
}
fn generate_inspector_token() -> String {
let mut bytes = [0u8; INSPECTOR_TOKEN_BYTES];
rand::thread_rng().fill_bytes(&mut bytes);
URL_SAFE_NO_PAD.encode(bytes)
}
fn configured_test_token() -> Option<String> {
std::env::var(INSPECTOR_TOKEN_ENV)
.ok()
.filter(|token| !token.is_empty())
.or_else(|| {
INSPECTOR_TEST_TOKEN_OVERRIDE
.get()
.and_then(|token| token.read().clone())
})
}
fn verify_token_bytes(candidate: &[u8], expected: &[u8]) -> Result<()> {
if candidate.ct_eq(expected).into() {
Ok(())
} else {
Err(InspectorUnauthorized.build())
}
}