use std::time::Duration;
use serde_json::{json, Map, Value};
use super::columns::ALLOWED_COLUMNS;
use super::error::MinterError;
pub const MIN_EXPIRES_IN: Duration = Duration::from_secs(60);
pub const MAX_EXPIRES_IN: Duration = Duration::from_secs(24 * 60 * 60);
#[derive(Clone, Debug, Default)]
pub struct TokenOptions {
pub tenant_id: String,
pub expires_in: Duration,
pub allowed_columns: Option<Vec<String>>,
pub allowed_actions: Option<Vec<String>>,
pub allowed_fields: Option<Vec<String>>,
pub allow_dsl_input: bool,
pub allow_nlp: bool,
}
impl TokenOptions {
pub fn to_wire(&self) -> Result<Value, MinterError> {
let mut w = Map::new();
if !self.tenant_id.is_empty() {
let t = self.tenant_id.trim();
if t.is_empty() {
return Err(invalid("tenant_id is empty after trim"));
}
if t.chars().count() > 256 {
return Err(invalid("tenant_id exceeds 256 chars"));
}
w.insert("tenant_id".into(), json!(t));
}
if !self.expires_in.is_zero() {
if self.expires_in < MIN_EXPIRES_IN {
return Err(invalid(format!(
"expires_in {:?} is below minimum {:?}",
self.expires_in, MIN_EXPIRES_IN
)));
}
if self.expires_in > MAX_EXPIRES_IN {
return Err(invalid(format!(
"expires_in {:?} is above maximum {:?}",
self.expires_in, MAX_EXPIRES_IN
)));
}
w.insert("expires_in".into(), json!(self.expires_in.as_secs()));
}
if let Some(cols) = &self.allowed_columns {
if cols.is_empty() {
return Err(invalid(
"allowed_columns is empty; use None for no restriction",
));
}
for c in cols {
if !ALLOWED_COLUMNS.contains(&c.as_str()) {
return Err(invalid(format!("unknown column name {c:?}")));
}
}
w.insert("columns".into(), json!(cols));
}
if let Some(actions) = &self.allowed_actions {
if actions.is_empty() {
return Err(invalid(
"allowed_actions is empty; use None for no restriction",
));
}
for a in actions {
if !valid_action(a) {
return Err(invalid(format!(
"action entry {a:?} does not match grammar \
[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)*(\\.\\*)?"
)));
}
}
w.insert("actions".into(), json!(actions));
}
if let Some(fields) = &self.allowed_fields {
if fields.is_empty() {
return Err(invalid(
"allowed_fields is empty; use None for no restriction",
));
}
w.insert("allowed_fields".into(), json!(fields));
}
if self.allow_dsl_input {
w.insert("allow_dsl_input".into(), json!(true));
}
if self.allow_nlp {
w.insert("allow_nlp".into(), json!(true));
}
Ok(Value::Object(w))
}
}
fn invalid(msg: impl Into<String>) -> MinterError {
MinterError::Validation(msg.into())
}
fn valid_action(a: &str) -> bool {
if a.is_empty() {
return false;
}
let segs: Vec<&str> = a.split('.').collect();
let last = segs.len() - 1;
for (i, seg) in segs.iter().enumerate() {
if i == last && *seg == "*" {
return segs.len() >= 2;
}
if seg.is_empty() || !seg.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_') {
return false;
}
}
true
}