Sync public tree to 4336e3b
This commit is contained in:
parent
009a9619e1
commit
d25447e17d
5 changed files with 414 additions and 6 deletions
|
|
@ -948,7 +948,7 @@ fn ping_coordinator(coordinator: &str, timeout: Duration) -> Result<Value> {
|
|||
fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
|
||||
let config = read_project_config(&cwd)?;
|
||||
let stored_session = read_cli_session(&cwd)?;
|
||||
let active_coordinator = args
|
||||
let configured_coordinator = args
|
||||
.scope
|
||||
.coordinator
|
||||
.clone()
|
||||
|
|
@ -961,7 +961,9 @@ fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
|
|||
stored_session
|
||||
.as_ref()
|
||||
.map(|session| session.coordinator.clone())
|
||||
})
|
||||
});
|
||||
let active_coordinator = configured_coordinator
|
||||
.clone()
|
||||
.unwrap_or_else(default_operator_endpoint);
|
||||
let tenant = effective_scope_value(
|
||||
&args.scope.tenant,
|
||||
|
|
@ -995,6 +997,21 @@ fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
|
|||
.or_else(|| stored_session.as_ref().map(|session| session.user.as_str())),
|
||||
"user",
|
||||
);
|
||||
let coordinator_account_status = configured_coordinator
|
||||
.as_ref()
|
||||
.map(|coordinator| {
|
||||
coordinator_auth_status_summary(coordinator, &tenant, &project, &principal)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
json!({
|
||||
"checked": false,
|
||||
"reason": "no project or session coordinator configured",
|
||||
"suspension_known": false,
|
||||
"account_status": "unknown",
|
||||
"private_moderation_details_exposed": false,
|
||||
"signup_failure_details_exposed": false,
|
||||
})
|
||||
});
|
||||
Ok(json!({
|
||||
"command": "auth status",
|
||||
"active_coordinator": active_coordinator,
|
||||
|
|
@ -1002,10 +1019,128 @@ fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
|
|||
"tenant": tenant,
|
||||
"project": project,
|
||||
"session": auth_state_value(&cwd)?,
|
||||
"coordinator_account_status": coordinator_account_status,
|
||||
"project_config": config,
|
||||
}))
|
||||
}
|
||||
|
||||
fn coordinator_auth_status_summary(
|
||||
coordinator: &str,
|
||||
tenant: &str,
|
||||
project: &str,
|
||||
principal: &str,
|
||||
) -> Value {
|
||||
let mut session = match JsonLineSession::connect(coordinator) {
|
||||
Ok(session) => session,
|
||||
Err(error) => {
|
||||
let message = error.to_string();
|
||||
return json!({
|
||||
"checked": true,
|
||||
"reachable": false,
|
||||
"source": "public_coordinator_api",
|
||||
"account_status": "unknown",
|
||||
"suspension_known": false,
|
||||
"private_moderation_details_exposed": false,
|
||||
"signup_failure_details_exposed": false,
|
||||
"machine_error": cli_error_summary(&message),
|
||||
"error": message,
|
||||
"next_actions": ["disasmer doctor", "check coordinator status"],
|
||||
"coordinator_session_requests": 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
let response = match session.request_allow_error(json!({
|
||||
"type": "auth_status",
|
||||
"tenant": tenant,
|
||||
"project": project,
|
||||
"actor_user": principal,
|
||||
})) {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
let message = error.to_string();
|
||||
return json!({
|
||||
"checked": true,
|
||||
"reachable": false,
|
||||
"source": "public_coordinator_api",
|
||||
"account_status": "unknown",
|
||||
"suspension_known": false,
|
||||
"private_moderation_details_exposed": false,
|
||||
"signup_failure_details_exposed": false,
|
||||
"machine_error": cli_error_summary(&message),
|
||||
"error": message,
|
||||
"next_actions": ["disasmer doctor", "check coordinator status"],
|
||||
"coordinator_session_requests": session.requests(),
|
||||
});
|
||||
}
|
||||
};
|
||||
let coordinator_session_requests = session.requests();
|
||||
if response.get("type").and_then(Value::as_str) == Some("error") {
|
||||
let message = response
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("coordinator rejected auth status");
|
||||
return json!({
|
||||
"checked": true,
|
||||
"reachable": true,
|
||||
"source": "public_coordinator_api",
|
||||
"account_status": "unknown",
|
||||
"suspension_known": false,
|
||||
"private_moderation_details_exposed": false,
|
||||
"signup_failure_details_exposed": false,
|
||||
"machine_error": cli_error_summary(message),
|
||||
"coordinator_response_type": "error",
|
||||
"next_actions": ["disasmer doctor", "disasmer login --browser"],
|
||||
"coordinator_session_requests": coordinator_session_requests,
|
||||
});
|
||||
}
|
||||
let suspended = response
|
||||
.get("suspended")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let disabled = response
|
||||
.get("disabled")
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(false);
|
||||
let account_status = response
|
||||
.get("account_status")
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| {
|
||||
if suspended {
|
||||
"suspended"
|
||||
} else if disabled {
|
||||
"disabled"
|
||||
} else {
|
||||
"active"
|
||||
}
|
||||
.to_owned()
|
||||
});
|
||||
let sanitized_reason = response.get("sanitized_reason").and_then(Value::as_str);
|
||||
let next_actions = response
|
||||
.get("next_actions")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|value| value.as_str().map(str::to_owned))
|
||||
.collect::<Vec<_>>();
|
||||
json!({
|
||||
"checked": true,
|
||||
"reachable": true,
|
||||
"source": "public_coordinator_api",
|
||||
"account_status": account_status,
|
||||
"suspension_known": true,
|
||||
"suspended": suspended,
|
||||
"disabled": disabled,
|
||||
"sanitized_reason": sanitized_reason,
|
||||
"next_actions": next_actions,
|
||||
"private_moderation_details_exposed": false,
|
||||
"signup_failure_details_exposed": false,
|
||||
"coordinator_response_type": response.get("type").and_then(Value::as_str).unwrap_or("auth_status"),
|
||||
"coordinator_session_requests": coordinator_session_requests,
|
||||
})
|
||||
}
|
||||
|
||||
fn auth_logout_report(args: AuthLogoutArgs, cwd: PathBuf) -> Result<Value> {
|
||||
logout_report(args, cwd, "auth logout")
|
||||
}
|
||||
|
|
@ -2741,6 +2876,25 @@ fn human_report(value: &Value) -> String {
|
|||
if let Some(session) = value.get("session") {
|
||||
lines.push(format!("session: {}", compact_json(session)));
|
||||
}
|
||||
if let Some(account) = value.get("coordinator_account_status") {
|
||||
if let Some(checked) = account.get("checked").and_then(Value::as_bool) {
|
||||
lines.push(format!("account status checked: {checked}"));
|
||||
}
|
||||
push_nested_string_field(&mut lines, account, "account_status", "account status");
|
||||
if let Some(suspended) = account.get("suspended").and_then(Value::as_bool) {
|
||||
lines.push(format!("account suspended: {suspended}"));
|
||||
}
|
||||
if let Some(disabled) = account.get("disabled").and_then(Value::as_bool) {
|
||||
lines.push(format!("account disabled: {disabled}"));
|
||||
}
|
||||
push_nested_string_field(&mut lines, account, "sanitized_reason", "account reason");
|
||||
if let Some(exposed) = account
|
||||
.get("private_moderation_details_exposed")
|
||||
.and_then(Value::as_bool)
|
||||
{
|
||||
lines.push(format!("private moderation details exposed: {exposed}"));
|
||||
}
|
||||
}
|
||||
if let Some(coordinator_selection) = value.get("coordinator") {
|
||||
if coordinator_selection.is_object() {
|
||||
lines.push(format!(
|
||||
|
|
@ -7530,11 +7684,29 @@ mod tests {
|
|||
#[test]
|
||||
fn auth_status_reads_stored_cli_session_without_provider_tokens() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap().to_string();
|
||||
let server = std::thread::spawn(move || {
|
||||
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(r#""tenant":"tenant-session""#));
|
||||
assert!(line.contains(r#""project":"project-session""#));
|
||||
assert!(line.contains(r#""actor_user":"user-session""#));
|
||||
stream
|
||||
.write_all(
|
||||
br#"{"type":"auth_status","tenant":"tenant-session","project":"project-session","actor":"user-session","authenticated":true,"account_status":"active","suspended":false,"disabled":false,"sanitized_reason":null,"next_actions":[],"private_moderation_details_exposed":false,"signup_failure_details_exposed":false}"#,
|
||||
)
|
||||
.unwrap();
|
||||
stream.write_all(b"\n").unwrap();
|
||||
});
|
||||
write_cli_session(
|
||||
temp.path(),
|
||||
&StoredCliSession {
|
||||
kind: "human".to_owned(),
|
||||
coordinator: "https://coord.example.test".to_owned(),
|
||||
coordinator: addr.clone(),
|
||||
tenant: "tenant-session".to_owned(),
|
||||
project: "project-session".to_owned(),
|
||||
user: "user-session".to_owned(),
|
||||
|
|
@ -7561,8 +7733,9 @@ mod tests {
|
|||
temp.path().to_path_buf(),
|
||||
)
|
||||
.unwrap();
|
||||
server.join().unwrap();
|
||||
|
||||
assert_eq!(report["active_coordinator"], "https://coord.example.test");
|
||||
assert_eq!(report["active_coordinator"], addr);
|
||||
assert_eq!(report["principal"], "user-session");
|
||||
assert_eq!(report["tenant"], "tenant-session");
|
||||
assert_eq!(report["project"], "project-session");
|
||||
|
|
@ -7576,6 +7749,97 @@ mod tests {
|
|||
assert_eq!(report["session"]["token_expiry_posture"], "expires_at");
|
||||
assert_eq!(report["session"]["provider_tokens_exposed_to_cli"], false);
|
||||
assert_eq!(report["session"]["provider_tokens_exposed_to_nodes"], false);
|
||||
assert_eq!(report["coordinator_account_status"]["checked"], true);
|
||||
assert_eq!(
|
||||
report["coordinator_account_status"]["account_status"],
|
||||
"active"
|
||||
);
|
||||
assert_eq!(
|
||||
report["coordinator_account_status"]["private_moderation_details_exposed"],
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_status_queries_coordinator_account_state_without_private_moderation_details() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap().to_string();
|
||||
let server = std::thread::spawn(move || {
|
||||
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(r#""tenant":"tenant-live""#));
|
||||
assert!(line.contains(r#""project":"project-live""#));
|
||||
assert!(line.contains(r#""actor_user":"user-live""#));
|
||||
stream
|
||||
.write_all(
|
||||
br#"{"type":"auth_status","tenant":"tenant-live","project":"project-live","actor":"user-live","authenticated":true,"account_status":"suspended","suspended":true,"disabled":false,"sanitized_reason":"account or tenant is suspended by hosted policy","next_actions":["contact the hosted operator"],"private_moderation_details_exposed":false,"signup_failure_details_exposed":false,"abuse_score":99,"moderation_notes":"private moderation note"}"#,
|
||||
)
|
||||
.unwrap();
|
||||
stream.write_all(b"\n").unwrap();
|
||||
});
|
||||
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let report = auth_status_report(
|
||||
AuthStatusArgs {
|
||||
scope: CliScopeArgs {
|
||||
coordinator: Some(addr),
|
||||
tenant: "tenant-live".to_owned(),
|
||||
project: "project-live".to_owned(),
|
||||
user: "user-live".to_owned(),
|
||||
json: false,
|
||||
},
|
||||
},
|
||||
temp.path().to_path_buf(),
|
||||
)
|
||||
.unwrap();
|
||||
server.join().unwrap();
|
||||
|
||||
assert_eq!(report["command"], "auth status");
|
||||
assert_eq!(report["coordinator_account_status"]["checked"], true);
|
||||
assert_eq!(report["coordinator_account_status"]["reachable"], true);
|
||||
assert_eq!(
|
||||
report["coordinator_account_status"]["source"],
|
||||
"public_coordinator_api"
|
||||
);
|
||||
assert_eq!(
|
||||
report["coordinator_account_status"]["account_status"],
|
||||
"suspended"
|
||||
);
|
||||
assert_eq!(report["coordinator_account_status"]["suspended"], true);
|
||||
assert_eq!(report["coordinator_account_status"]["disabled"], false);
|
||||
assert_eq!(
|
||||
report["coordinator_account_status"]["sanitized_reason"],
|
||||
"account or tenant is suspended by hosted policy"
|
||||
);
|
||||
assert_eq!(
|
||||
report["coordinator_account_status"]["private_moderation_details_exposed"],
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
report["coordinator_account_status"]["signup_failure_details_exposed"],
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
report["coordinator_account_status"]["coordinator_response_type"],
|
||||
"auth_status"
|
||||
);
|
||||
assert_eq!(
|
||||
report["coordinator_account_status"]["coordinator_session_requests"],
|
||||
1
|
||||
);
|
||||
let serialized = serde_json::to_string(&report).unwrap();
|
||||
assert!(!serialized.contains("abuse_score"));
|
||||
assert!(!serialized.contains("moderation_notes"));
|
||||
assert!(!serialized.contains("private moderation note"));
|
||||
|
||||
let rendered = human_report(&report);
|
||||
assert!(rendered.contains("account status: suspended"));
|
||||
assert!(rendered.contains("account suspended: true"));
|
||||
assert!(rendered.contains("private moderation details exposed: false"));
|
||||
assert!(!rendered.contains("private moderation note"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ const DEBUG_CONTROL_READ_BYTES: u64 = 1024;
|
|||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum CoordinatorRequest {
|
||||
Ping,
|
||||
AuthStatus {
|
||||
tenant: String,
|
||||
project: String,
|
||||
actor_user: String,
|
||||
},
|
||||
AdminStatus {
|
||||
tenant: String,
|
||||
actor_user: String,
|
||||
|
|
@ -489,6 +494,19 @@ pub enum CoordinatorResponse {
|
|||
Pong {
|
||||
epoch: u64,
|
||||
},
|
||||
AuthStatus {
|
||||
tenant: TenantId,
|
||||
project: ProjectId,
|
||||
actor: UserId,
|
||||
authenticated: bool,
|
||||
account_status: String,
|
||||
suspended: bool,
|
||||
disabled: bool,
|
||||
sanitized_reason: Option<String>,
|
||||
next_actions: Vec<String>,
|
||||
private_moderation_details_exposed: bool,
|
||||
signup_failure_details_exposed: bool,
|
||||
},
|
||||
AdminStatus {
|
||||
tenant: TenantId,
|
||||
actor: UserId,
|
||||
|
|
@ -781,6 +799,40 @@ impl CoordinatorService {
|
|||
CoordinatorRequest::Ping => Ok(CoordinatorResponse::Pong {
|
||||
epoch: self.coordinator.coordinator_epoch(),
|
||||
}),
|
||||
CoordinatorRequest::AuthStatus {
|
||||
tenant,
|
||||
project,
|
||||
actor_user,
|
||||
} => {
|
||||
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()
|
||||
};
|
||||
Ok(CoordinatorResponse::AuthStatus {
|
||||
tenant,
|
||||
project,
|
||||
actor,
|
||||
authenticated: true,
|
||||
account_status,
|
||||
suspended,
|
||||
disabled: false,
|
||||
sanitized_reason,
|
||||
next_actions,
|
||||
private_moderation_details_exposed: false,
|
||||
signup_failure_details_exposed: false,
|
||||
})
|
||||
}
|
||||
CoordinatorRequest::AdminStatus { tenant, actor_user } => {
|
||||
let tenant = TenantId::new(tenant);
|
||||
let actor = UserId::new(actor_user);
|
||||
|
|
@ -2909,6 +2961,39 @@ mod tests {
|
|||
fn service_reports_and_enforces_public_admin_tenant_suspension() {
|
||||
let mut service = CoordinatorService::new(7);
|
||||
|
||||
let CoordinatorResponse::AuthStatus {
|
||||
tenant,
|
||||
project,
|
||||
actor,
|
||||
authenticated,
|
||||
account_status,
|
||||
suspended,
|
||||
disabled,
|
||||
sanitized_reason,
|
||||
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 auth status");
|
||||
};
|
||||
assert_eq!(tenant, TenantId::from("tenant"));
|
||||
assert_eq!(project, ProjectId::from("project"));
|
||||
assert_eq!(actor, UserId::from("user"));
|
||||
assert!(authenticated);
|
||||
assert_eq!(account_status, "active");
|
||||
assert!(!suspended);
|
||||
assert!(!disabled);
|
||||
assert!(sanitized_reason.is_none());
|
||||
assert!(!private_moderation_details_exposed);
|
||||
assert!(!signup_failure_details_exposed);
|
||||
|
||||
let CoordinatorResponse::AdminStatus {
|
||||
tenant,
|
||||
actor,
|
||||
|
|
@ -2957,6 +3042,38 @@ mod tests {
|
|||
};
|
||||
assert!(suspended);
|
||||
|
||||
let CoordinatorResponse::AuthStatus {
|
||||
account_status,
|
||||
suspended,
|
||||
disabled,
|
||||
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 suspended auth status");
|
||||
};
|
||||
assert_eq!(account_status, "suspended");
|
||||
assert!(suspended);
|
||||
assert!(!disabled);
|
||||
assert_eq!(
|
||||
sanitized_reason.as_deref(),
|
||||
Some("account or tenant is suspended by hosted policy")
|
||||
);
|
||||
assert!(next_actions
|
||||
.iter()
|
||||
.any(|action| action.contains("hosted operator")));
|
||||
assert!(!private_moderation_details_exposed);
|
||||
assert!(!signup_failure_details_exposed);
|
||||
|
||||
let create = service
|
||||
.handle_request(CoordinatorRequest::CreateProject {
|
||||
tenant: "tenant".to_owned(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue