use crate::action::{Error, Result};
use crate::ctx::{BoxFuture, Ctx, Service};
use crate::feature::{Guard, GuardResult};
use crate::i18n::Text;
use crate::time::Date;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeSet, HashMap};
use std::fmt;
use std::sync::{Arc, Mutex};
#[derive(Clone, PartialEq, Eq)]
pub struct Secret<T>(T);
impl<T> Secret<T> {
pub fn new(value: T) -> Self {
Secret(value)
}
pub fn expose(&self) -> &T {
&self.0
}
}
impl<T> fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[redacted]")
}
}
impl<T> fmt::Display for Secret<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("[redacted]")
}
}
pub trait ConfigSource: Send + Sync + 'static {
fn name(&self) -> &str;
fn load(&self) -> BoxFuture<std::result::Result<HashMap<String, String>, String>>;
}
pub struct EnvSource(pub &'static str);
impl ConfigSource for EnvSource {
fn name(&self) -> &str {
"environment"
}
fn load(&self) -> BoxFuture<std::result::Result<HashMap<String, String>, String>> {
let prefix = format!("{}_", self.0);
let values = std::env::vars()
.filter_map(|(k, v)| k.strip_prefix(&prefix).map(|rest| (rest.to_lowercase().replace("__", "."), v)))
.collect();
Box::pin(async move { Ok(values) })
}
}
pub struct JsonFileSource(pub std::path::PathBuf);
impl ConfigSource for JsonFileSource {
fn name(&self) -> &str {
"json file"
}
fn load(&self) -> BoxFuture<std::result::Result<HashMap<String, String>, String>> {
let path = self.0.clone();
Box::pin(async move {
let text = std::fs::read_to_string(&path).map_err(|e| format!("{}: {e}", path.display()))?;
let json: serde_json::Value =
serde_json::from_str(&text).map_err(|e| format!("{}: {e}", path.display()))?;
let mut out = HashMap::new();
flatten("", &json, &mut out);
Ok(out)
})
}
}
pub struct StaticSource(pub HashMap<String, String>);
impl ConfigSource for StaticSource {
fn name(&self) -> &str {
"static"
}
fn load(&self) -> BoxFuture<std::result::Result<HashMap<String, String>, String>> {
let values = self.0.clone();
Box::pin(async move { Ok(values) })
}
}
fn flatten(prefix: &str, value: &serde_json::Value, out: &mut HashMap<String, String>) {
match value {
serde_json::Value::Object(map) => {
for (k, v) in map {
let key = if prefix.is_empty() { k.to_lowercase() } else { format!("{prefix}.{}", k.to_lowercase()) };
flatten(&key, v, out);
}
}
serde_json::Value::String(s) => {
out.insert(prefix.to_string(), s.clone());
}
serde_json::Value::Null => {}
other => {
out.insert(prefix.to_string(), other.to_string());
}
}
}
#[derive(Clone, Default)]
pub struct Config {
values: Arc<HashMap<String, String>>,
origin: Arc<HashMap<String, String>>,
}
impl fmt::Debug for Config {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut keys: Vec<&String> = self.values.keys().collect();
keys.sort();
write!(f, "Config {{ keys: {keys:?} }}")
}
}
impl Service for Config {}
impl Config {
pub async fn load(sources: Vec<Box<dyn ConfigSource>>) -> std::result::Result<Config, String> {
let mut values = HashMap::new();
let mut origin = HashMap::new();
for source in sources {
let loaded = source.load().await.map_err(|e| format!("config source `{}`: {e}", source.name()))?;
for (k, v) in loaded {
origin.insert(k.clone(), source.name().to_string());
values.insert(k, v);
}
}
Ok(Config { values: Arc::new(values), origin: Arc::new(origin) })
}
pub fn get(&self, key: &str) -> Option<&str> {
self.values.get(key).map(String::as_str)
}
pub fn require(&self, key: &str) -> Result<&str> {
self.get(key).ok_or_else(|| Error::Internal(format!("configuration `{key}` is missing")))
}
pub fn secret(&self, key: &str) -> Result<Secret<String>> {
self.require(key).map(|v| Secret::new(v.to_string()))
}
pub fn source_of(&self, key: &str) -> Option<&str> {
self.origin.get(key).map(String::as_str)
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.values.keys().map(String::as_str)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub struct Tenant {
id: String,
}
impl Tenant {
pub fn new(id: &str) -> Option<Tenant> {
let ok = (1..=63).contains(&id.len())
&& id.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-' || b == b'_');
ok.then(|| Tenant { id: id.to_string() })
}
pub fn id(&self) -> &str {
&self.id
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct TenantRequired;
impl Guard for TenantRequired {
fn check(&self, ctx: &Ctx) -> GuardResult {
match ctx.tenant() {
Some(_) => GuardResult::Allow,
None => GuardResult::Deny(Text::key("zeyn.error.tenant")),
}
}
fn name(&self) -> &'static str {
"TenantRequired"
}
}
type Factory<T> = Arc<dyn Fn(Tenant, Config) -> BoxFuture<Result<T>> + Send + Sync>;
pub struct PerTenant<T> {
cache: Arc<Mutex<HashMap<String, T>>>,
make: Factory<T>,
}
impl<T> Clone for PerTenant<T> {
fn clone(&self) -> Self {
PerTenant { cache: self.cache.clone(), make: self.make.clone() }
}
}
impl<T: Clone + Send + Sync + 'static> Service for PerTenant<T> {}
impl<T: Clone + Send + Sync + 'static> PerTenant<T> {
pub fn new(make: impl Fn(Tenant, Config) -> BoxFuture<Result<T>> + Send + Sync + 'static) -> Self {
PerTenant { cache: Arc::new(Mutex::new(HashMap::new())), make: Arc::new(make) }
}
pub async fn get(&self, ctx: &Ctx) -> Result<T> {
let tenant = ctx.tenant().cloned().ok_or(Error::Denied(Text::key("zeyn.error.tenant")))?;
if let Some(v) =
self.cache.lock().map_err(|_| Error::Internal("tenant cache poisoned".into()))?.get(tenant.id())
{
return Ok(v.clone());
}
let value = (self.make)(tenant.clone(), ctx.config().clone()).await?;
self.cache
.lock()
.map_err(|_| Error::Internal("tenant cache poisoned".into()))?
.entry(tenant.id().to_string())
.or_insert(value.clone());
Ok(value)
}
pub fn forget(&self, tenant: &Tenant) {
if let Ok(mut c) = self.cache.lock() {
c.remove(tenant.id());
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct License {
pub holder: String,
pub valid_until: Date,
#[serde(default)]
pub modules: BTreeSet<String>,
#[serde(default)]
pub max_users: Option<u32>,
}
impl License {
pub fn is_valid_on(&self, day: Date) -> bool {
day <= self.valid_until
}
pub fn allows(&self, module: &str) -> bool {
self.modules.is_empty() || self.modules.contains(module)
}
}
pub trait LicenseSource: Send + Sync + 'static {
fn load(&self, tenant: Option<&Tenant>) -> BoxFuture<std::result::Result<Option<License>, String>>;
}
pub struct FixedLicense(pub License);
impl LicenseSource for FixedLicense {
fn load(&self, _tenant: Option<&Tenant>) -> BoxFuture<std::result::Result<Option<License>, String>> {
let l = self.0.clone();
Box::pin(async move { Ok(Some(l)) })
}
}
#[derive(Clone, Debug)]
pub struct Licensed(Option<&'static str>);
impl Licensed {
pub fn any() -> Self {
Licensed(None)
}
pub fn module(name: &'static str) -> Self {
Licensed(Some(name))
}
}
impl Guard for Licensed {
fn check(&self, ctx: &Ctx) -> GuardResult {
match ctx.license() {
Some(l) if !l.is_valid_on(ctx.today()) => GuardResult::Deny(Text::key("zeyn.error.license_expired")),
Some(l) if self.0.is_none_or(|m| l.allows(m)) => GuardResult::Allow,
_ => GuardResult::Deny(Text::key("zeyn.error.license")),
}
}
fn name(&self) -> &'static str {
"Licensed"
}
}
#[cfg(feature = "signed-licenses")]
const LICENSE_CONTEXT: &[u8] = b"zeyn-license-v1\n";
#[cfg(feature = "signed-licenses")]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LicenseError {
InvalidPublicKey,
Malformed(String),
BadSignature,
}
#[cfg(feature = "signed-licenses")]
impl fmt::Display for LicenseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LicenseError::InvalidPublicKey => f.write_str("the license public key in the app is not a valid ed25519 key (64 hex digits)"),
LicenseError::Malformed(why) => write!(f, "the license file is not a signed license: {why}"),
LicenseError::BadSignature => {
f.write_str("the license signature does not match: the file was changed after signing or was signed with another key")
}
}
}
}
#[cfg(feature = "signed-licenses")]
impl std::error::Error for LicenseError {}
#[cfg(feature = "signed-licenses")]
fn from_hex<const N: usize>(hex: &str) -> Option<[u8; N]> {
let hex = hex.trim().as_bytes();
if hex.len() != N * 2 {
return None;
}
let mut out = [0u8; N];
for (i, pair) in hex.chunks(2).enumerate() {
out[i] = u8::from_str_radix(std::str::from_utf8(pair).ok()?, 16).ok()?;
}
Some(out)
}
#[cfg(feature = "signed-licenses")]
impl License {
pub fn from_signed(file: &str, public_key: &str) -> std::result::Result<License, LicenseError> {
let key = from_hex::<32>(public_key)
.and_then(|k| ed25519_dalek::VerifyingKey::from_bytes(&k).ok())
.ok_or(LicenseError::InvalidPublicKey)?;
#[derive(Deserialize)]
struct Signed {
payload: String,
signature: String,
}
let signed: Signed = serde_json::from_str(file).map_err(|e| LicenseError::Malformed(e.to_string()))?;
let signature = from_hex::<64>(&signed.signature)
.ok_or_else(|| LicenseError::Malformed("`signature` is not 128 hex digits".into()))?;
let mut message = Vec::with_capacity(LICENSE_CONTEXT.len() + signed.payload.len());
message.extend_from_slice(LICENSE_CONTEXT);
message.extend_from_slice(signed.payload.as_bytes());
key.verify_strict(&message, &ed25519_dalek::Signature::from_bytes(&signature))
.map_err(|_| LicenseError::BadSignature)?;
serde_json::from_str(&signed.payload).map_err(|e| LicenseError::Malformed(format!("payload: {e}")))
}
}
#[cfg(feature = "signed-licenses")]
pub struct SignedLicenseFile {
path: std::path::PathBuf,
public_key: &'static str,
}
#[cfg(feature = "signed-licenses")]
impl SignedLicenseFile {
pub fn new(path: impl Into<std::path::PathBuf>, public_key: &'static str) -> Self {
SignedLicenseFile { path: path.into(), public_key }
}
}
#[cfg(feature = "signed-licenses")]
impl LicenseSource for SignedLicenseFile {
fn load(&self, _tenant: Option<&Tenant>) -> BoxFuture<std::result::Result<Option<License>, String>> {
let result =
std::fs::read_to_string(&self.path).map_err(|e| format!("{}: {e}", self.path.display())).map(|text| {
License::from_signed(&text, self.public_key)
.inspect_err(|e| tracing::error!("license {} refused, the app is locked: {e}", self.path.display()))
.ok()
});
Box::pin(async move { result })
}
}