use serde::{Deserialize, Serialize};
use crate::membership::{GrantLog, Refused, Subject, check};
use crate::rights::{Mode, Resource};
pub const BOUND_INVALID: &str = "membership_page_bound_invalid";
pub const ROW_OVER_BUDGET: &str = "membership_page_row_over_budget";
pub const CURSOR_FOREIGN: &str = "membership_cursor_foreign";
pub const CURSOR_RESET: &str = "membership_cursor_reset";
pub const CURSOR_MALFORMED: &str = "membership_cursor_malformed";
pub const CURSOR_EXPIRED: &str = "membership_cursor_expired";
pub const CURSORS_FULL: &str = "membership_cursors_full";
pub const CURSORS_UNAVAILABLE: &str = "membership_cursors_unavailable";
pub const PAGE_OVER_CEILING: &str = "membership_page_over_ceiling";
pub const PAGES_UNCONFIGURED: &str = "membership_pages_unconfigured";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipPageBounds))]
pub struct PageBounds {
pub rows: u32,
pub bytes: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipResourcePageRequest))]
pub struct ResourcePageRequest {
pub contract: u32,
pub log: GrantLog,
pub workspace: Resource,
pub subject: Subject,
pub kind: String,
pub action: String,
pub at_least: u64,
pub bounds: PageBounds,
pub after: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipRecipientPageRequest))]
pub struct RecipientPageRequest {
pub contract: u32,
pub log: GrantLog,
pub workspace: Resource,
pub resource: Resource,
pub action: String,
pub at_least: u64,
pub bounds: PageBounds,
pub after: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipPageRow))]
pub struct PageRow {
pub subject: Subject,
pub resource: Resource,
pub grant: String,
pub scope: Resource,
pub mode: Mode,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipPageOutcome))]
pub enum PageOutcome {
Page {
rows: Vec<PageRow>,
returned: u64,
skipped: u64,
complete: bool,
next: Option<String>,
},
Refused {
refusal: String,
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema), schema(as = MembershipPage))]
pub struct MembershipPage {
pub contract: u32,
pub log: GrantLog,
pub revision: Option<u64>,
pub outcome: PageOutcome,
}
pub fn within_ceiling(bounds: PageBounds, ceiling: PageBounds) -> Result<(), Refused> {
if bounds.rows > ceiling.rows || bounds.bytes > ceiling.bytes {
return Err(Refused {
name: PAGE_OVER_CEILING,
reason: format!(
"a page is bounded by at most {} rows and {} bytes here",
ceiling.rows, ceiling.bytes
),
});
}
Ok(())
}
fn bounded(bounds: PageBounds) -> Result<(), Refused> {
if bounds.rows == 0 || bounds.bytes == 0 {
return Err(Refused {
name: BOUND_INVALID,
reason: "a page is bounded by at least one row and one byte".to_owned(),
});
}
Ok(())
}
pub fn validate_resources(request: &ResourcePageRequest, served: &GrantLog) -> Result<(), Refused> {
let mut members = vec![
("workspace kind", request.workspace.kind.as_str()),
("workspace id", request.workspace.id.as_str()),
("subject id", request.subject.id.as_str()),
("subject kind", request.subject.kind.as_str()),
("kind", request.kind.as_str()),
("action", request.action.as_str()),
];
if let Some(after) = &request.after {
members.push(("cursor", after.as_str()));
}
check(request.contract, &request.log, served, &members)?;
bounded(request.bounds)
}
pub fn validate_recipients(
request: &RecipientPageRequest,
served: &GrantLog,
) -> Result<(), Refused> {
let mut members = vec![
("workspace kind", request.workspace.kind.as_str()),
("workspace id", request.workspace.id.as_str()),
("resource kind", request.resource.kind.as_str()),
("resource id", request.resource.id.as_str()),
("action", request.action.as_str()),
];
if let Some(after) = &request.after {
members.push(("cursor", after.as_str()));
}
check(request.contract, &request.log, served, &members)?;
bounded(request.bounds)
}
#[must_use]
pub fn refused_page(served: &GrantLog, name: &str, reason: &str) -> MembershipPage {
MembershipPage {
contract: crate::membership::CONTRACT_VERSION,
log: served.clone(),
revision: None,
outcome: PageOutcome::Refused {
refusal: name.to_owned(),
reason: reason.to_owned(),
},
}
}