use async_trait::async_trait;
use cheers_core::{PrincipalId, PrincipalKind, StoreError};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum OwnershipValidationError {
#[error("granted_by must be a service principal; got {0}")]
GrantedByNotService(PrincipalKind),
#[error("on_behalf_of must be a user principal when set; got {0}")]
OnBehalfOfNotUser(PrincipalKind),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct NewOwnership {
pub principal_id: PrincipalId,
pub resource_kind: String,
pub resource_id: String,
pub relationship: String,
pub granted_by: PrincipalId,
pub on_behalf_of: Option<PrincipalId>,
}
impl NewOwnership {
pub fn new(
principal_id: PrincipalId,
resource_kind: impl Into<String>,
resource_id: impl Into<String>,
relationship: impl Into<String>,
granted_by: PrincipalId,
on_behalf_of: Option<PrincipalId>,
) -> Result<Self, OwnershipValidationError> {
if granted_by.kind != PrincipalKind::Service {
return Err(OwnershipValidationError::GrantedByNotService(
granted_by.kind,
));
}
if let Some(ref obo) = on_behalf_of {
if obo.kind != PrincipalKind::User {
return Err(OwnershipValidationError::OnBehalfOfNotUser(obo.kind));
}
}
Ok(Self {
principal_id,
resource_kind: resource_kind.into(),
resource_id: resource_id.into(),
relationship: relationship.into(),
granted_by,
on_behalf_of,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OwnershipRow {
pub id: String,
pub principal_id: PrincipalId,
pub resource_kind: String,
pub resource_id: String,
pub relationship: String,
pub granted_by: PrincipalId,
pub on_behalf_of: Option<PrincipalId>,
pub granted_at: i64,
pub revoked_at: Option<i64>,
}
impl OwnershipRow {
#[allow(clippy::too_many_arguments)]
pub fn new(
id: String,
principal_id: PrincipalId,
resource_kind: String,
resource_id: String,
relationship: String,
granted_by: PrincipalId,
on_behalf_of: Option<PrincipalId>,
granted_at: i64,
revoked_at: Option<i64>,
) -> Self {
Self {
id,
principal_id,
resource_kind,
resource_id,
relationship,
granted_by,
on_behalf_of,
granted_at,
revoked_at,
}
}
pub fn is_revoked(&self) -> bool {
self.revoked_at.is_some()
}
}
#[async_trait]
pub trait OwnershipStore: Send + Sync {
async fn insert(
&self,
ownership: &NewOwnership,
now: i64,
) -> Result<OwnershipRow, StoreError>;
async fn get(&self, id: &str) -> Result<Option<OwnershipRow>, StoreError>;
async fn revoke_by_id(&self, id: &str, now: i64) -> Result<(), StoreError>;
async fn revoke_by_on_behalf_of(
&self,
user: &PrincipalId,
now: i64,
) -> Result<u64, StoreError>;
async fn list_for_principal(
&self,
principal: &PrincipalId,
) -> Result<Vec<OwnershipRow>, StoreError>;
async fn list_for_resource(
&self,
resource_kind: &str,
resource_id: &str,
) -> Result<Vec<OwnershipRow>, StoreError>;
}
#[cfg(test)]
mod tests {
use super::*;
fn user(id: &str) -> PrincipalId {
PrincipalId::user(id)
}
fn svc(id: &str) -> PrincipalId {
PrincipalId::service(id)
}
fn camp(id: &str) -> PrincipalId {
PrincipalId::camp(id)
}
#[test]
fn new_ownership_accepts_service_granted_by_and_user_on_behalf_of() {
let n = NewOwnership::new(
camp("c-1"),
"service",
"svc-abc",
"owns",
svc("yubaba"),
Some(user("alice")),
)
.unwrap();
assert_eq!(n.granted_by, svc("yubaba"));
assert_eq!(n.on_behalf_of, Some(user("alice")));
NewOwnership::new(camp("c-2"), "service", "s-1", "owns", svc("yubaba"), None).unwrap();
}
#[test]
fn new_ownership_rejects_user_granted_by() {
let err = NewOwnership::new(
camp("c-1"),
"service",
"svc-abc",
"owns",
user("alice"),
Some(user("alice")),
)
.unwrap_err();
assert_eq!(
err,
OwnershipValidationError::GrantedByNotService(PrincipalKind::User)
);
let err = NewOwnership::new(
camp("c-1"),
"service",
"svc-abc",
"owns",
camp("c-1"),
None,
)
.unwrap_err();
assert_eq!(
err,
OwnershipValidationError::GrantedByNotService(PrincipalKind::Camp)
);
}
#[test]
fn new_ownership_rejects_non_user_on_behalf_of() {
let err = NewOwnership::new(
camp("c-1"),
"service",
"svc-abc",
"owns",
svc("yubaba"),
Some(svc("yubaba")),
)
.unwrap_err();
assert_eq!(
err,
OwnershipValidationError::OnBehalfOfNotUser(PrincipalKind::Service)
);
let err = NewOwnership::new(
camp("c-1"),
"service",
"svc-abc",
"owns",
svc("yubaba"),
Some(camp("c-9")),
)
.unwrap_err();
assert_eq!(
err,
OwnershipValidationError::OnBehalfOfNotUser(PrincipalKind::Camp)
);
}
#[test]
fn ownership_row_is_revoked_tracks_revoked_at() {
let mut row = OwnershipRow::new(
"id-1".into(),
camp("c-1"),
"service".into(),
"svc-a".into(),
"owns".into(),
svc("yubaba"),
Some(user("alice")),
100,
None,
);
assert!(!row.is_revoked());
row.revoked_at = Some(150);
assert!(row.is_revoked());
}
}