use gunnar_proto::entitlement_server::Entitlement;
use gunnar_proto::{
EntitlementFact, EntitlementState, GetEntitlementRequest, GetEntitlementResponse, GrantLevel,
ListEntitlementsRequest, ListEntitlementsResponse, SetEntitlementRequest,
SetEntitlementResponse,
};
use tonic::{Request, Response, Status};
use crate::auth::Access;
use crate::service::EntitlementService;
pub fn well_formed(fact: &EntitlementFact) -> Result<(), Status> {
if fact.namespace.is_empty() {
return Err(Status::invalid_argument("fact.namespace is required"));
}
if fact.plan.is_empty() {
return Err(Status::invalid_argument("fact.plan is required"));
}
if fact.state() == EntitlementState::Unspecified {
return Err(Status::invalid_argument(
"fact.state is unspecified; a fact names one of Free, Paid, Grace, Suspended, Retention",
));
}
if fact.source.is_empty() {
return Err(Status::invalid_argument(
"fact.source is required: `operator` or `payment:<vendor>:<reference>`",
));
}
if fact.signature.len() != 64 {
return Err(Status::invalid_argument(format!(
"fact.signature is {} byte(s); an Ed25519 signature is 64",
fact.signature.len()
)));
}
match (fact.issued_unix_ms, fact.issued_signature.len()) {
(None, 0) | (Some(_), 64) => {}
(None, n) => {
return Err(Status::invalid_argument(format!(
"fact.issued_signature is {n} byte(s) but fact.issued_unix_ms is absent; \
a fact carries both halves of the stamp or neither"
)))
}
(Some(_), n) => {
return Err(Status::invalid_argument(format!(
"fact.issued_signature is {n} byte(s); the Ed25519 signature over \
fact.issued_unix_ms is 64"
)))
}
}
let unknown = crate::service::meters::caps_gunnar_cannot_honour(fact.caps.keys());
if !unknown.is_empty() {
return Err(Status::invalid_argument(format!(
"fact.caps names {} this appliance can neither enforce nor report, so a tenant \
would be sold a number nothing on this box ever reads; it honours {:?}",
unknown
.iter()
.map(|meter| format!("`{meter}`"))
.collect::<Vec<_>>()
.join(", "),
crate::service::meters::honourable_caps(),
)));
}
Ok(())
}
#[tonic::async_trait]
impl Entitlement for EntitlementService {
async fn get(
&self,
request: Request<GetEntitlementRequest>,
) -> Result<Response<GetEntitlementResponse>, Status> {
let caller = self.ctx.caller(&request)?;
let namespace = request.into_inner().namespace;
if namespace.is_empty() {
return Err(Status::invalid_argument("namespace is required"));
}
self.ctx.require(
&caller,
Access::namespace(&namespace),
GrantLevel::Read,
"Entitlement.Get",
)?;
let fact = self.ctx.control.entitlement_get(&namespace)?;
Ok(Response::new(GetEntitlementResponse { fact }))
}
async fn set(
&self,
request: Request<SetEntitlementRequest>,
) -> Result<Response<SetEntitlementResponse>, Status> {
let caller = self.ctx.authorize(
&request,
Access::Server,
GrantLevel::Admin,
"Entitlement.Set",
)?;
let fact = request
.into_inner()
.fact
.ok_or_else(|| Status::invalid_argument("fact is required"))?;
well_formed(&fact)?;
let namespace = fact.namespace.clone();
if self.ctx.control.log_follows() {
return Err(Status::unavailable(
"txlog: this box is a twin (or restoring); it originates no transactions — the \
fact is taken once it opens as the primary: try again",
));
}
if self.ctx.control.namespace(&namespace)?.is_none() {
return Err(Status::not_found(format!(
"there is no namespace {namespace} on this Gunnar, so there is no account to set \
a plan for: an entitlement is set for an account that exists. Nothing was \
verified, logged or stored"
)));
}
let source = fact.source.clone();
let before = self.ctx.control.entitlement_get(&namespace).ok().flatten();
let (plan, paid_until, caps) = (
fact.plan.clone(),
fact.paid_until_unix_ms,
fact.caps.clone(),
);
let (state, attestation_seq) = self.ctx.control.entitlement_set(fact)?;
let kind = match &before {
None => "billing.bought",
Some(prev) if prev.plan != plan || prev.caps != caps => "billing.upgraded",
Some(prev) if paid_until.unwrap_or(0) > prev.paid_until_unix_ms.unwrap_or(0) => {
"billing.prolonged"
}
Some(_) => "billing.changed",
};
self.ctx.account_event(
self.ctx.account_of_namespace(&namespace),
crate::enterprise::AccountAuditEvent::by(&caller, kind, namespace.clone()).detail(
format!(
"plan {plan}, {state:?}{}, {source}",
paid_until
.map(|t| format!(", paid until {t}"))
.unwrap_or_default()
),
),
);
tracing::info!(
target: crate::auth::AUDIT_TARGET,
event = "entitlement.set",
principal = %caller.principal_id,
%namespace,
state = ?state,
%source,
attestation_seq,
"entitlement fact accepted"
);
Ok(Response::new(SetEntitlementResponse {
state: state as i32,
attestation_seq,
}))
}
async fn list(
&self,
request: Request<ListEntitlementsRequest>,
) -> Result<Response<ListEntitlementsResponse>, Status> {
self.ctx.authorize(
&request,
Access::Server,
GrantLevel::Read,
"Entitlement.List",
)?;
let facts = self.ctx.control.entitlement_list()?;
let refused_at_boot = self.ctx.control.entitlement_refused_at_boot()?;
Ok(Response::new(ListEntitlementsResponse {
facts,
refused_at_boot,
}))
}
}