pub mod cert;
pub mod column_policy_gate;
pub mod locks;
pub mod middleware;
pub mod oauth;
pub mod policies;
pub mod privileges;
pub mod scope_cache;
pub mod scram;
pub mod store;
pub mod vault;
pub use scope_cache::{AuthCache, AuthCacheStats, ScopeKey, DEFAULT_TTL as DEFAULT_SCOPE_TTL};
pub use cert::{
CertAuthConfig, CertAuthError, CertAuthenticator, CertIdentity, CertIdentityMode,
ParsedClientCert,
};
pub use column_policy_gate::{
ColumnAccessRequest, ColumnDecision, ColumnDecisionEffect, ColumnPolicyGate,
ColumnPolicyOutcome, ColumnRef,
};
pub use oauth::{
DecodedJwt, Jwk, JwtClaims, JwtHeader, OAuthConfig, OAuthError, OAuthIdentity,
OAuthIdentityMode, OAuthValidator,
};
pub use privileges::{
check_grant, Action, AuthzContext, AuthzError, Grant, GrantPrincipal, GrantsView,
PermissionCache, Resource, UserAttributes,
};
pub use store::AuthStore;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Role {
Read,
Write,
Admin,
}
impl Role {
pub fn as_str(&self) -> &'static str {
match self {
Self::Read => "read",
Self::Write => "write",
Self::Admin => "admin",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"read" => Some(Self::Read),
"write" => Some(Self::Write),
"admin" => Some(Self::Admin),
_ => None,
}
}
pub fn can_read(&self) -> bool {
true
}
pub fn can_write(&self) -> bool {
matches!(self, Self::Write | Self::Admin)
}
pub fn can_admin(&self) -> bool {
matches!(self, Self::Admin)
}
}
impl fmt::Display for Role {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct User {
pub username: String,
pub tenant_id: Option<String>,
pub password_hash: String,
pub scram_verifier: Option<scram::ScramVerifier>,
pub role: Role,
pub api_keys: Vec<ApiKey>,
pub created_at: u128,
pub updated_at: u128,
pub enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct UserId {
pub tenant: Option<String>,
pub username: String,
}
impl UserId {
pub fn platform(name: impl Into<String>) -> Self {
Self {
tenant: None,
username: name.into(),
}
}
pub fn scoped(tenant: impl Into<String>, name: impl Into<String>) -> Self {
Self {
tenant: Some(tenant.into()),
username: name.into(),
}
}
pub fn from_parts(tenant: Option<&str>, username: &str) -> Self {
Self {
tenant: tenant.map(|t| t.to_string()),
username: username.to_string(),
}
}
}
impl fmt::Display for UserId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.tenant {
Some(t) => write!(f, "{}/{}", t, self.username),
None => f.write_str(&self.username),
}
}
}
#[derive(Debug, Clone)]
pub struct ApiKey {
pub key: String,
pub name: String,
pub role: Role,
pub created_at: u128,
}
#[derive(Debug, Clone)]
pub struct Session {
pub token: String,
pub username: String,
pub tenant_id: Option<String>,
pub role: Role,
pub created_at: u128,
pub expires_at: u128,
}
#[derive(Debug, Clone)]
pub struct AuthConfig {
pub enabled: bool,
pub session_ttl_secs: u64,
pub require_auth: bool,
pub auto_encrypt_storage: bool,
pub vault_enabled: bool,
pub cert: CertAuthConfig,
pub oauth: OAuthConfig,
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
enabled: false,
session_ttl_secs: 3600,
require_auth: false,
auto_encrypt_storage: false,
vault_enabled: false,
cert: CertAuthConfig::default(),
oauth: OAuthConfig::default(),
}
}
}
#[derive(Debug, Clone)]
pub enum AuthError {
UserExists(String),
UserNotFound(String),
InvalidCredentials,
KeyNotFound(String),
RoleExceeded { requested: Role, ceiling: Role },
Disabled,
Forbidden(String),
Internal(String),
}
impl fmt::Display for AuthError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UserExists(u) => write!(f, "user already exists: {u}"),
Self::UserNotFound(u) => write!(f, "user not found: {u}"),
Self::InvalidCredentials => write!(f, "invalid credentials"),
Self::KeyNotFound(k) => write!(f, "api key not found: {k}"),
Self::RoleExceeded { requested, ceiling } => {
write!(
f,
"requested role '{requested}' exceeds ceiling '{ceiling}'"
)
}
Self::Disabled => write!(f, "authentication is disabled"),
Self::Forbidden(msg) => write!(f, "forbidden: {msg}"),
Self::Internal(msg) => write!(f, "internal auth error: {msg}"),
}
}
}
impl std::error::Error for AuthError {}
pub(crate) fn now_ms() -> u128 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_role_ordering() {
assert!(Role::Read < Role::Write);
assert!(Role::Write < Role::Admin);
}
#[test]
fn test_role_roundtrip() {
for role in [Role::Read, Role::Write, Role::Admin] {
assert_eq!(Role::from_str(role.as_str()), Some(role));
}
assert_eq!(Role::from_str("unknown"), None);
}
#[test]
fn test_role_permissions() {
assert!(Role::Read.can_read());
assert!(!Role::Read.can_write());
assert!(!Role::Read.can_admin());
assert!(Role::Write.can_read());
assert!(Role::Write.can_write());
assert!(!Role::Write.can_admin());
assert!(Role::Admin.can_read());
assert!(Role::Admin.can_write());
assert!(Role::Admin.can_admin());
}
#[test]
fn test_auth_config_default() {
let cfg = AuthConfig::default();
assert!(!cfg.enabled);
assert_eq!(cfg.session_ttl_secs, 3600);
assert!(!cfg.require_auth);
assert!(!cfg.auto_encrypt_storage);
}
#[test]
fn test_auth_error_display() {
let err = AuthError::UserExists("alice".into());
assert!(err.to_string().contains("alice"));
let err = AuthError::InvalidCredentials;
assert!(err.to_string().contains("invalid"));
}
}