Sync public tree to 7aeb51f
Source commit: 7aeb51ff791bd445afc2d51757239b6e306db015 Public tree identity: sha256:c0ab7127634af6502afe357d48a0336bf2f5777be43877097e395df73ef3c5a9
This commit is contained in:
parent
81942fa2ad
commit
8c11db913e
5 changed files with 357 additions and 25 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"kind": "disasmer-filtered-public-tree",
|
"kind": "disasmer-filtered-public-tree",
|
||||||
"source_commit": "bc3642c6900c2551532a7e2263e822fd1c668ba8",
|
"source_commit": "7aeb51ff791bd445afc2d51757239b6e306db015",
|
||||||
"release_name": "dryrun-bc3642c6900c",
|
"release_name": "dryrun-7aeb51ff791b",
|
||||||
"filtered_out": [
|
"filtered_out": [
|
||||||
"private/**",
|
"private/**",
|
||||||
"experiments/**",
|
"experiments/**",
|
||||||
|
|
|
||||||
|
|
@ -1007,6 +1007,7 @@ fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
|
||||||
"checked": false,
|
"checked": false,
|
||||||
"reason": "no project or session coordinator configured",
|
"reason": "no project or session coordinator configured",
|
||||||
"suspension_known": false,
|
"suspension_known": false,
|
||||||
|
"account_state_known": false,
|
||||||
"account_status": "unknown",
|
"account_status": "unknown",
|
||||||
"private_moderation_details_exposed": false,
|
"private_moderation_details_exposed": false,
|
||||||
"signup_failure_details_exposed": false,
|
"signup_failure_details_exposed": false,
|
||||||
|
|
@ -1040,6 +1041,7 @@ fn coordinator_auth_status_summary(
|
||||||
"source": "public_coordinator_api",
|
"source": "public_coordinator_api",
|
||||||
"account_status": "unknown",
|
"account_status": "unknown",
|
||||||
"suspension_known": false,
|
"suspension_known": false,
|
||||||
|
"account_state_known": false,
|
||||||
"private_moderation_details_exposed": false,
|
"private_moderation_details_exposed": false,
|
||||||
"signup_failure_details_exposed": false,
|
"signup_failure_details_exposed": false,
|
||||||
"machine_error": cli_error_summary(&message),
|
"machine_error": cli_error_summary(&message),
|
||||||
|
|
@ -1064,6 +1066,7 @@ fn coordinator_auth_status_summary(
|
||||||
"source": "public_coordinator_api",
|
"source": "public_coordinator_api",
|
||||||
"account_status": "unknown",
|
"account_status": "unknown",
|
||||||
"suspension_known": false,
|
"suspension_known": false,
|
||||||
|
"account_state_known": false,
|
||||||
"private_moderation_details_exposed": false,
|
"private_moderation_details_exposed": false,
|
||||||
"signup_failure_details_exposed": false,
|
"signup_failure_details_exposed": false,
|
||||||
"machine_error": cli_error_summary(&message),
|
"machine_error": cli_error_summary(&message),
|
||||||
|
|
@ -1085,6 +1088,7 @@ fn coordinator_auth_status_summary(
|
||||||
"source": "public_coordinator_api",
|
"source": "public_coordinator_api",
|
||||||
"account_status": "unknown",
|
"account_status": "unknown",
|
||||||
"suspension_known": false,
|
"suspension_known": false,
|
||||||
|
"account_state_known": false,
|
||||||
"private_moderation_details_exposed": false,
|
"private_moderation_details_exposed": false,
|
||||||
"signup_failure_details_exposed": false,
|
"signup_failure_details_exposed": false,
|
||||||
"machine_error": cli_error_summary(message),
|
"machine_error": cli_error_summary(message),
|
||||||
|
|
@ -1101,15 +1105,27 @@ fn coordinator_auth_status_summary(
|
||||||
.get("disabled")
|
.get("disabled")
|
||||||
.and_then(Value::as_bool)
|
.and_then(Value::as_bool)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
let deleted = response
|
||||||
|
.get("deleted")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(false);
|
||||||
|
let manual_review = response
|
||||||
|
.get("manual_review")
|
||||||
|
.and_then(Value::as_bool)
|
||||||
|
.unwrap_or(false);
|
||||||
let account_status = response
|
let account_status = response
|
||||||
.get("account_status")
|
.get("account_status")
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.map(str::to_owned)
|
.map(str::to_owned)
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
if suspended {
|
if deleted {
|
||||||
"suspended"
|
"deleted"
|
||||||
} else if disabled {
|
} else if disabled {
|
||||||
"disabled"
|
"disabled"
|
||||||
|
} else if suspended {
|
||||||
|
"suspended"
|
||||||
|
} else if manual_review {
|
||||||
|
"manual_review"
|
||||||
} else {
|
} else {
|
||||||
"active"
|
"active"
|
||||||
}
|
}
|
||||||
|
|
@ -1130,8 +1146,11 @@ fn coordinator_auth_status_summary(
|
||||||
"source": "public_coordinator_api",
|
"source": "public_coordinator_api",
|
||||||
"account_status": account_status,
|
"account_status": account_status,
|
||||||
"suspension_known": true,
|
"suspension_known": true,
|
||||||
|
"account_state_known": true,
|
||||||
"suspended": suspended,
|
"suspended": suspended,
|
||||||
"disabled": disabled,
|
"disabled": disabled,
|
||||||
|
"deleted": deleted,
|
||||||
|
"manual_review": manual_review,
|
||||||
"sanitized_reason": sanitized_reason,
|
"sanitized_reason": sanitized_reason,
|
||||||
"next_actions": next_actions,
|
"next_actions": next_actions,
|
||||||
"private_moderation_details_exposed": false,
|
"private_moderation_details_exposed": false,
|
||||||
|
|
@ -2887,6 +2906,12 @@ fn human_report(value: &Value) -> String {
|
||||||
if let Some(disabled) = account.get("disabled").and_then(Value::as_bool) {
|
if let Some(disabled) = account.get("disabled").and_then(Value::as_bool) {
|
||||||
lines.push(format!("account disabled: {disabled}"));
|
lines.push(format!("account disabled: {disabled}"));
|
||||||
}
|
}
|
||||||
|
if let Some(deleted) = account.get("deleted").and_then(Value::as_bool) {
|
||||||
|
lines.push(format!("account deleted: {deleted}"));
|
||||||
|
}
|
||||||
|
if let Some(manual_review) = account.get("manual_review").and_then(Value::as_bool) {
|
||||||
|
lines.push(format!("account manual review: {manual_review}"));
|
||||||
|
}
|
||||||
push_nested_string_field(&mut lines, account, "sanitized_reason", "account reason");
|
push_nested_string_field(&mut lines, account, "sanitized_reason", "account reason");
|
||||||
if let Some(exposed) = account
|
if let Some(exposed) = account
|
||||||
.get("private_moderation_details_exposed")
|
.get("private_moderation_details_exposed")
|
||||||
|
|
@ -7842,6 +7867,117 @@ mod tests {
|
||||||
assert!(!rendered.contains("private moderation note"));
|
assert!(!rendered.contains("private moderation note"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn auth_status_reports_disabled_deleted_and_manual_review_safely() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||||
|
let addr = listener.local_addr().unwrap().to_string();
|
||||||
|
let server = std::thread::spawn(move || {
|
||||||
|
for (tenant, status, reason, flags) in [
|
||||||
|
(
|
||||||
|
"tenant-disabled",
|
||||||
|
"disabled",
|
||||||
|
"account or tenant is disabled by hosted policy",
|
||||||
|
(false, true, false, false),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tenant-deleted",
|
||||||
|
"deleted",
|
||||||
|
"account or tenant is no longer active",
|
||||||
|
(false, false, true, false),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"tenant-review",
|
||||||
|
"manual_review",
|
||||||
|
"account or tenant is pending hosted review",
|
||||||
|
(false, false, false, true),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
|
let mut reader = BufReader::new(stream.try_clone().unwrap());
|
||||||
|
let mut line = String::new();
|
||||||
|
reader.read_line(&mut line).unwrap();
|
||||||
|
assert!(line.contains(r#""type":"auth_status""#));
|
||||||
|
assert!(line.contains(&format!(r#""tenant":"{tenant}""#)));
|
||||||
|
let (suspended, disabled, deleted, manual_review) = flags;
|
||||||
|
writeln!(
|
||||||
|
stream,
|
||||||
|
"{}",
|
||||||
|
json!({
|
||||||
|
"type": "auth_status",
|
||||||
|
"tenant": tenant,
|
||||||
|
"project": "project-live",
|
||||||
|
"actor": "user-live",
|
||||||
|
"authenticated": true,
|
||||||
|
"account_status": status,
|
||||||
|
"suspended": suspended,
|
||||||
|
"disabled": disabled,
|
||||||
|
"deleted": deleted,
|
||||||
|
"manual_review": manual_review,
|
||||||
|
"sanitized_reason": reason,
|
||||||
|
"next_actions": ["contact the hosted operator"],
|
||||||
|
"private_moderation_details_exposed": false,
|
||||||
|
"signup_failure_details_exposed": false,
|
||||||
|
"abuse_score": 99,
|
||||||
|
"moderation_notes": "private moderation note",
|
||||||
|
"signup_policy_trace": "private signup trace",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (tenant, status, rendered_marker) in [
|
||||||
|
("tenant-disabled", "disabled", "account disabled: true"),
|
||||||
|
("tenant-deleted", "deleted", "account deleted: true"),
|
||||||
|
(
|
||||||
|
"tenant-review",
|
||||||
|
"manual_review",
|
||||||
|
"account manual review: true",
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
let report = auth_status_report(
|
||||||
|
AuthStatusArgs {
|
||||||
|
scope: CliScopeArgs {
|
||||||
|
coordinator: Some(addr.clone()),
|
||||||
|
tenant: tenant.to_owned(),
|
||||||
|
project: "project-live".to_owned(),
|
||||||
|
user: "user-live".to_owned(),
|
||||||
|
json: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
temp.path().to_path_buf(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
report["coordinator_account_status"]["account_status"],
|
||||||
|
status
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report["coordinator_account_status"]["account_state_known"],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report["coordinator_account_status"]["private_moderation_details_exposed"],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
report["coordinator_account_status"]["signup_failure_details_exposed"],
|
||||||
|
false
|
||||||
|
);
|
||||||
|
let serialized = serde_json::to_string(&report).unwrap();
|
||||||
|
assert!(!serialized.contains("abuse_score"));
|
||||||
|
assert!(!serialized.contains("moderation_notes"));
|
||||||
|
assert!(!serialized.contains("signup_policy_trace"));
|
||||||
|
assert!(!serialized.contains("private moderation note"));
|
||||||
|
let rendered = human_report(&report);
|
||||||
|
assert!(rendered.contains(&format!("account status: {status}")));
|
||||||
|
assert!(rendered.contains(rendered_marker));
|
||||||
|
assert!(!rendered.contains("private moderation note"));
|
||||||
|
}
|
||||||
|
server.join().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cli_first_mvp_command_surface_parses() {
|
fn cli_first_mvp_command_surface_parses() {
|
||||||
for args in [
|
for args in [
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,17 @@ pub struct ServicePolicyRecord {
|
||||||
pub digest: Digest,
|
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)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct ProjectPermissionRecord {
|
pub struct ProjectPermissionRecord {
|
||||||
pub tenant: TenantId,
|
pub tenant: TenantId,
|
||||||
|
|
@ -353,11 +364,71 @@ impl Coordinator {
|
||||||
.is_some()
|
.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> {
|
pub fn ensure_tenant_active(&self, tenant: &TenantId) -> Result<(), CoordinatorError> {
|
||||||
if self.tenant_suspended(tenant) {
|
let account_state = self.account_policy_state(tenant);
|
||||||
return Err(CoordinatorError::Unauthorized(
|
if account_state.account_status != "active" {
|
||||||
"tenant is suspended by admin controls".to_owned(),
|
return Err(CoordinatorError::Unauthorized(format!(
|
||||||
));
|
"tenant is {} by admin controls",
|
||||||
|
account_state.account_status
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -953,6 +1024,63 @@ mod tests {
|
||||||
assert!(restarted.tenant_suspended(&TenantId::from("tenant")));
|
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]
|
#[test]
|
||||||
fn agent_public_keys_are_project_user_scoped_and_restart_durable() {
|
fn agent_public_keys_are_project_user_scoped_and_restart_durable() {
|
||||||
let mut store = InMemoryDurableStore::default();
|
let mut store = InMemoryDurableStore::default();
|
||||||
|
|
|
||||||
|
|
@ -502,6 +502,8 @@ pub enum CoordinatorResponse {
|
||||||
account_status: String,
|
account_status: String,
|
||||||
suspended: bool,
|
suspended: bool,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
|
deleted: bool,
|
||||||
|
manual_review: bool,
|
||||||
sanitized_reason: Option<String>,
|
sanitized_reason: Option<String>,
|
||||||
next_actions: Vec<String>,
|
next_actions: Vec<String>,
|
||||||
private_moderation_details_exposed: bool,
|
private_moderation_details_exposed: bool,
|
||||||
|
|
@ -807,28 +809,19 @@ impl CoordinatorService {
|
||||||
let tenant = TenantId::new(tenant);
|
let tenant = TenantId::new(tenant);
|
||||||
let project = ProjectId::new(project);
|
let project = ProjectId::new(project);
|
||||||
let actor = UserId::new(actor_user);
|
let actor = UserId::new(actor_user);
|
||||||
let suspended = self.coordinator.tenant_suspended(&tenant);
|
let account_state = self.coordinator.account_policy_state(&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()
|
|
||||||
};
|
|
||||||
Ok(CoordinatorResponse::AuthStatus {
|
Ok(CoordinatorResponse::AuthStatus {
|
||||||
tenant,
|
tenant,
|
||||||
project,
|
project,
|
||||||
actor,
|
actor,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
account_status,
|
account_status: account_state.account_status,
|
||||||
suspended,
|
suspended: account_state.suspended,
|
||||||
disabled: false,
|
disabled: account_state.disabled,
|
||||||
sanitized_reason,
|
deleted: account_state.deleted,
|
||||||
next_actions,
|
manual_review: account_state.manual_review,
|
||||||
|
sanitized_reason: account_state.sanitized_reason,
|
||||||
|
next_actions: account_state.next_actions,
|
||||||
private_moderation_details_exposed: false,
|
private_moderation_details_exposed: false,
|
||||||
signup_failure_details_exposed: false,
|
signup_failure_details_exposed: false,
|
||||||
})
|
})
|
||||||
|
|
@ -2994,6 +2987,65 @@ mod tests {
|
||||||
assert!(!private_moderation_details_exposed);
|
assert!(!private_moderation_details_exposed);
|
||||||
assert!(!signup_failure_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 {
|
let CoordinatorResponse::AdminStatus {
|
||||||
tenant,
|
tenant,
|
||||||
actor,
|
actor,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ function expect(source, name, pattern) {
|
||||||
const criteria = read("cli_acceptance_criteria.md");
|
const criteria = read("cli_acceptance_criteria.md");
|
||||||
const cli = read("crates/disasmer-cli/src/main.rs");
|
const cli = read("crates/disasmer-cli/src/main.rs");
|
||||||
const coordinator = read("crates/disasmer-coordinator/src/service.rs");
|
const coordinator = read("crates/disasmer-coordinator/src/service.rs");
|
||||||
|
const coordinatorLib = read("crates/disasmer-coordinator/src/lib.rs");
|
||||||
const cliFirstAcceptance = read("scripts/acceptance-cli-first.sh");
|
const cliFirstAcceptance = read("scripts/acceptance-cli-first.sh");
|
||||||
const outputModeSmoke = read("scripts/cli-output-mode-smoke.js");
|
const outputModeSmoke = read("scripts/cli-output-mode-smoke.js");
|
||||||
const errorExitSmoke = read("scripts/cli-error-exit-smoke.js");
|
const errorExitSmoke = read("scripts/cli-error-exit-smoke.js");
|
||||||
|
|
@ -96,6 +97,11 @@ expect(
|
||||||
"hosted signup sanitized failure criteria",
|
"hosted signup sanitized failure criteria",
|
||||||
/Signup failures do not disclose moderation, abuse-scoring, or allow\/deny internals[\s\S]*stripping issuer, allowlist, abuse-score, moderation-note, and provider-secret details/
|
/Signup failures do not disclose moderation, abuse-scoring, or allow\/deny internals[\s\S]*stripping issuer, allowlist, abuse-score, moderation-note, and provider-secret details/
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
criteria,
|
||||||
|
"hosted account state privacy criteria",
|
||||||
|
/Account suspension, deletion, manual review, and abuse handling are private hosted-admin concerns[\s\S]*public CLI displays clear suspended, disabled, deleted, and manual-review status[\s\S]*strips private moderation\/signup details/
|
||||||
|
);
|
||||||
|
|
||||||
const lines = criterionLines(criteria);
|
const lines = criterionLines(criteria);
|
||||||
assert(lines.length > 0, "CLI-first criteria must contain criteria lines");
|
assert(lines.length > 0, "CLI-first criteria must contain criteria lines");
|
||||||
|
|
@ -507,6 +513,16 @@ expect(
|
||||||
"CLI auth status does not expose raw private moderation response fields",
|
"CLI auth status does not expose raw private moderation response fields",
|
||||||
/fn auth_status_queries_coordinator_account_state_without_private_moderation_details[\s\S]*abuse_score[\s\S]*moderation_notes[\s\S]*!serialized\.contains\("abuse_score"\)[\s\S]*!serialized\.contains\("moderation_notes"\)/
|
/fn auth_status_queries_coordinator_account_state_without_private_moderation_details[\s\S]*abuse_score[\s\S]*moderation_notes[\s\S]*!serialized\.contains\("abuse_score"\)[\s\S]*!serialized\.contains\("moderation_notes"\)/
|
||||||
);
|
);
|
||||||
|
expect(
|
||||||
|
cli,
|
||||||
|
"CLI auth status reports inactive account states safely",
|
||||||
|
/fn auth_status_reports_disabled_deleted_and_manual_review_safely[\s\S]*account_status[\s\S]*disabled[\s\S]*deleted[\s\S]*manual_review[\s\S]*!serialized\.contains\("signup_policy_trace"\)/
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
coordinatorLib,
|
||||||
|
"coordinator summarizes private account policy state safely",
|
||||||
|
/fn tenant_disabled\(&self, tenant: &TenantId\) -> bool[\s\S]*tenant:disabled[\s\S]*fn tenant_deleted\(&self, tenant: &TenantId\) -> bool[\s\S]*tenant:deleted[\s\S]*fn tenant_manual_review\(&self, tenant: &TenantId\) -> bool[\s\S]*tenant:manual_review[\s\S]*fn account_policy_state\(&self, tenant: &TenantId\) -> AccountPolicyState[\s\S]*"deleted"[\s\S]*"disabled"[\s\S]*"manual_review"[\s\S]*"account or tenant is pending hosted review"/
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
cli,
|
cli,
|
||||||
"CLI renders sanitized account status",
|
"CLI renders sanitized account status",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue