use std::sync::Arc;
use dashmap::DashMap;
use tonic::Status;
use crate::server::auth::Principal;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(super) enum Owner {
Anonymous,
User(Arc<str>),
}
impl Owner {
pub(super) fn from_principal(principal: Option<&Principal>) -> Self {
principal.map_or(Self::Anonymous, |principal| Self::User(Arc::from(principal.username.as_str())))
}
}
#[derive(Debug, Default)]
pub(super) struct QueryOwnership {
owners: DashMap<String, Owner>,
}
impl QueryOwnership {
pub(super) fn insert(&self, id: String, owner: Owner) {
self.owners.insert(id, owner);
}
pub(super) fn authorize(&self, id: &str, caller: &Owner) -> Result<(), Status> {
match self.owners.get(id) {
Some(owner) if owner.value() != caller => {
Err(Status::permission_denied("query id or prepared statement handle is not accessible"))
},
_ => Ok(()),
}
}
pub(super) fn is_owned_by(&self, id: &str, caller: &Owner) -> bool {
self.owners.get(id).is_some_and(|owner| owner.value() == caller)
}
pub(super) fn remove(&self, id: &str) {
self.owners.remove(id);
}
pub(super) fn retain_live(&self, is_live: impl Fn(&str) -> bool) {
self.owners.retain(|id, _| is_live(id));
}
#[cfg(test)]
pub(super) fn len(&self) -> usize {
self.owners.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn user(name: &str) -> Owner {
Owner::from_principal(Some(&Principal { username: name.to_string() }))
}
#[test]
fn owner_rejects_a_different_principal() {
let ownership = QueryOwnership::default();
ownership.insert("q1".to_string(), user("alice"));
let status = ownership.authorize("q1", &user("mallory")).unwrap_err();
assert_eq!(status.code(), tonic::Code::PermissionDenied);
ownership.authorize("q1", &user("alice")).expect("the issuing principal keeps access");
}
#[test]
fn disabled_auth_shares_one_owner() {
let ownership = QueryOwnership::default();
ownership.insert("q1".to_string(), Owner::Anonymous);
ownership.authorize("q1", &Owner::Anonymous).expect("anonymous callers share every id");
}
#[test]
fn anonymous_and_named_principals_are_distinct_owners() {
let ownership = QueryOwnership::default();
ownership.insert("q1".to_string(), Owner::Anonymous);
assert_eq!(ownership.authorize("q1", &user("alice")).unwrap_err().code(), tonic::Code::PermissionDenied);
}
#[test]
fn unregistered_id_defers_to_the_lookup_that_follows() {
let ownership = QueryOwnership::default();
ownership.authorize("never-issued", &user("alice")).expect("unknown ids are resolved by their own lookup");
}
#[test]
fn removed_id_stops_being_owned() {
let ownership = QueryOwnership::default();
ownership.insert("q1".to_string(), user("alice"));
ownership.remove("q1");
assert_eq!(ownership.len(), 0);
ownership.authorize("q1", &user("mallory")).expect("a released id is no longer owned by anyone");
}
}