Sync public tree to 7aeb51f

Source commit: 7aeb51ff791bd445afc2d51757239b6e306db015

Public tree identity: sha256:c0ab7127634af6502afe357d48a0336bf2f5777be43877097e395df73ef3c5a9
This commit is contained in:
Michel Paulissen 2026-07-04 15:45:28 +02:00
parent 81942fa2ad
commit 8c11db913e
5 changed files with 357 additions and 25 deletions

View file

@ -71,6 +71,17 @@ pub struct ServicePolicyRecord {
pub digest: Digest,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AccountPolicyState {
pub account_status: String,
pub suspended: bool,
pub disabled: bool,
pub deleted: bool,
pub manual_review: bool,
pub sanitized_reason: Option<String>,
pub next_actions: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProjectPermissionRecord {
pub tenant: TenantId,
@ -353,11 +364,71 @@ impl Coordinator {
.is_some()
}
pub fn tenant_disabled(&self, tenant: &TenantId) -> bool {
self.service_policy_record(tenant, "tenant:disabled")
.is_some()
}
pub fn tenant_deleted(&self, tenant: &TenantId) -> bool {
self.service_policy_record(tenant, "tenant:deleted")
.is_some()
}
pub fn tenant_manual_review(&self, tenant: &TenantId) -> bool {
self.service_policy_record(tenant, "tenant:manual_review")
.is_some()
}
pub fn account_policy_state(&self, tenant: &TenantId) -> AccountPolicyState {
let suspended = self.tenant_suspended(tenant);
let disabled = self.tenant_disabled(tenant);
let deleted = self.tenant_deleted(tenant);
let manual_review = self.tenant_manual_review(tenant);
let account_status = if deleted {
"deleted"
} else if disabled {
"disabled"
} else if suspended {
"suspended"
} else if manual_review {
"manual_review"
} else {
"active"
}
.to_owned();
let sanitized_reason = match account_status.as_str() {
"deleted" => Some("account or tenant is no longer active".to_owned()),
"disabled" => Some("account or tenant is disabled by hosted policy".to_owned()),
"suspended" => Some("account or tenant is suspended by hosted policy".to_owned()),
"manual_review" => Some("account or tenant is pending hosted review".to_owned()),
_ => None,
};
let next_actions = if account_status == "active" {
Vec::new()
} else {
vec![
"disasmer auth status --json".to_owned(),
"contact the hosted operator or use a self-hosted coordinator".to_owned(),
]
};
AccountPolicyState {
account_status,
suspended,
disabled,
deleted,
manual_review,
sanitized_reason,
next_actions,
}
}
pub fn ensure_tenant_active(&self, tenant: &TenantId) -> Result<(), CoordinatorError> {
if self.tenant_suspended(tenant) {
return Err(CoordinatorError::Unauthorized(
"tenant is suspended by admin controls".to_owned(),
));
let account_state = self.account_policy_state(tenant);
if account_state.account_status != "active" {
return Err(CoordinatorError::Unauthorized(format!(
"tenant is {} by admin controls",
account_state.account_status
)));
}
Ok(())
}
@ -953,6 +1024,63 @@ mod tests {
assert!(restarted.tenant_suspended(&TenantId::from("tenant")));
}
#[test]
fn account_policy_state_summarizes_private_admin_records_safely() {
let tenant = TenantId::from("tenant");
let mut coordinator = Coordinator::boot(&InMemoryDurableStore::default(), 1);
let active = coordinator.account_policy_state(&tenant);
assert_eq!(active.account_status, "active");
assert!(active.sanitized_reason.is_none());
assert!(active.next_actions.is_empty());
coordinator.upsert_service_policy_record(
tenant.clone(),
"tenant:manual_review",
Digest::from_parts([b"manual-review".as_slice()]),
);
let manual_review = coordinator.account_policy_state(&tenant);
assert_eq!(manual_review.account_status, "manual_review");
assert!(manual_review.manual_review);
assert_eq!(
manual_review.sanitized_reason.as_deref(),
Some("account or tenant is pending hosted review")
);
coordinator.upsert_service_policy_record(
tenant.clone(),
"tenant:disabled",
Digest::from_parts([b"disabled".as_slice()]),
);
let disabled = coordinator.account_policy_state(&tenant);
assert_eq!(disabled.account_status, "disabled");
assert!(disabled.disabled);
assert!(disabled.manual_review);
assert_eq!(
disabled.sanitized_reason.as_deref(),
Some("account or tenant is disabled by hosted policy")
);
coordinator.upsert_service_policy_record(
tenant.clone(),
"tenant:deleted",
Digest::from_parts([b"deleted".as_slice()]),
);
let deleted = coordinator.account_policy_state(&tenant);
assert_eq!(deleted.account_status, "deleted");
assert!(deleted.deleted);
assert!(deleted.disabled);
assert!(deleted.manual_review);
assert_eq!(
deleted.sanitized_reason.as_deref(),
Some("account or tenant is no longer active")
);
assert!(deleted
.next_actions
.iter()
.any(|action| action.contains("hosted operator")));
}
#[test]
fn agent_public_keys_are_project_user_scoped_and_restart_durable() {
let mut store = InMemoryDurableStore::default();

View file

@ -502,6 +502,8 @@ pub enum CoordinatorResponse {
account_status: String,
suspended: bool,
disabled: bool,
deleted: bool,
manual_review: bool,
sanitized_reason: Option<String>,
next_actions: Vec<String>,
private_moderation_details_exposed: bool,
@ -807,28 +809,19 @@ impl CoordinatorService {
let tenant = TenantId::new(tenant);
let project = ProjectId::new(project);
let actor = UserId::new(actor_user);
let suspended = self.coordinator.tenant_suspended(&tenant);
let account_status = if suspended { "suspended" } else { "active" }.to_owned();
let sanitized_reason =
suspended.then(|| "account or tenant is suspended by hosted policy".to_owned());
let next_actions = if suspended {
vec![
"disasmer auth status --json".to_owned(),
"contact the hosted operator or use a self-hosted coordinator".to_owned(),
]
} else {
Vec::new()
};
let account_state = self.coordinator.account_policy_state(&tenant);
Ok(CoordinatorResponse::AuthStatus {
tenant,
project,
actor,
authenticated: true,
account_status,
suspended,
disabled: false,
sanitized_reason,
next_actions,
account_status: account_state.account_status,
suspended: account_state.suspended,
disabled: account_state.disabled,
deleted: account_state.deleted,
manual_review: account_state.manual_review,
sanitized_reason: account_state.sanitized_reason,
next_actions: account_state.next_actions,
private_moderation_details_exposed: false,
signup_failure_details_exposed: false,
})
@ -2994,6 +2987,65 @@ mod tests {
assert!(!private_moderation_details_exposed);
assert!(!signup_failure_details_exposed);
for (tenant, policy_name, expected_status, expected_reason) in [
(
"manual-tenant",
"tenant:manual_review",
"manual_review",
"account or tenant is pending hosted review",
),
(
"disabled-tenant",
"tenant:disabled",
"disabled",
"account or tenant is disabled by hosted policy",
),
(
"deleted-tenant",
"tenant:deleted",
"deleted",
"account or tenant is no longer active",
),
] {
service.coordinator.upsert_service_policy_record(
TenantId::from(tenant),
policy_name,
Digest::from_parts([tenant.as_bytes(), policy_name.as_bytes()]),
);
let CoordinatorResponse::AuthStatus {
account_status,
suspended,
disabled,
deleted,
manual_review,
sanitized_reason,
next_actions,
private_moderation_details_exposed,
signup_failure_details_exposed,
..
} = service
.handle_request(CoordinatorRequest::AuthStatus {
tenant: tenant.to_owned(),
project: "project".to_owned(),
actor_user: "user".to_owned(),
})
.unwrap()
else {
panic!("expected inactive auth status");
};
assert_eq!(account_status, expected_status);
assert_eq!(suspended, expected_status == "suspended");
assert_eq!(disabled, expected_status == "disabled");
assert_eq!(deleted, expected_status == "deleted");
assert_eq!(manual_review, expected_status == "manual_review");
assert_eq!(sanitized_reason.as_deref(), Some(expected_reason));
assert!(next_actions
.iter()
.any(|action| action.contains("hosted operator")));
assert!(!private_moderation_details_exposed);
assert!(!signup_failure_details_exposed);
}
let CoordinatorResponse::AdminStatus {
tenant,
actor,