delete agent flow
This commit is contained in:
@@ -102,6 +102,7 @@ List/revoke enroll tokens from CLI:
|
|||||||
```sh
|
```sh
|
||||||
wakey-control-plane list-enroll-tokens --include-expired
|
wakey-control-plane list-enroll-tokens --include-expired
|
||||||
wakey-control-plane revoke-enroll-token --token enr-...
|
wakey-control-plane revoke-enroll-token --token enr-...
|
||||||
|
wakey-control-plane revoke-agent --agent-id agent-...
|
||||||
```
|
```
|
||||||
|
|
||||||
Machine-readable output is available:
|
Machine-readable output is available:
|
||||||
@@ -194,6 +195,7 @@ Control-plane admin API includes token management endpoints:
|
|||||||
- `GET /api/v1/control/alerts?lookback_seconds=900`
|
- `GET /api/v1/control/alerts?lookback_seconds=900`
|
||||||
- `GET /api/v1/control/alerts/history?since_unix=<ts>&limit=<n>`
|
- `GET /api/v1/control/alerts/history?since_unix=<ts>&limit=<n>`
|
||||||
- `GET /api/v1/control/alerts/ws` (websocket snapshots + recent transitions)
|
- `GET /api/v1/control/alerts/ws` (websocket snapshots + recent transitions)
|
||||||
|
- `DELETE /api/v1/control/agents/{agent_id}`
|
||||||
|
|
||||||
If commands still appear silent, verify both processes are running with `-v`
|
If commands still appear silent, verify both processes are running with `-v`
|
||||||
and that `RUST_LOG` is not overriding to a stricter level.
|
and that `RUST_LOG` is not overriding to a stricter level.
|
||||||
|
|||||||
+14
-1
@@ -10,6 +10,7 @@ import {
|
|||||||
fetchAlertHistory,
|
fetchAlertHistory,
|
||||||
fetchAlerts,
|
fetchAlerts,
|
||||||
fetchAudit,
|
fetchAudit,
|
||||||
|
revokeAgent,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import { AppLayout } from "@/layout/AppLayout";
|
import { AppLayout } from "@/layout/AppLayout";
|
||||||
import { AgentsPage } from "@/pages/AgentsPage";
|
import { AgentsPage } from "@/pages/AgentsPage";
|
||||||
@@ -47,7 +48,12 @@ export function App() {
|
|||||||
setAlerts(nextAlerts);
|
setAlerts(nextAlerts);
|
||||||
setHistory(nextHistory);
|
setHistory(nextHistory);
|
||||||
setAudit(nextAudit);
|
setAudit(nextAudit);
|
||||||
if (!selectedAgentId && nextAgents[0]) {
|
if (!nextAgents.length) {
|
||||||
|
setSelectedAgentId("");
|
||||||
|
} else if (
|
||||||
|
!selectedAgentId ||
|
||||||
|
!nextAgents.some((agent) => agent.agent_id === selectedAgentId)
|
||||||
|
) {
|
||||||
setSelectedAgentId(nextAgents[0].agent_id);
|
setSelectedAgentId(nextAgents[0].agent_id);
|
||||||
}
|
}
|
||||||
setState("ready");
|
setState("ready");
|
||||||
@@ -57,6 +63,12 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onRevokeAgent(agentId: string): Promise<boolean> {
|
||||||
|
const result = await revokeAgent(agentId);
|
||||||
|
await loadAll();
|
||||||
|
return result.revoked;
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshAlertsAndHistory() {
|
async function refreshAlertsAndHistory() {
|
||||||
const [nextAlerts, nextHistory] = await Promise.all([
|
const [nextAlerts, nextHistory] = await Promise.all([
|
||||||
fetchAlerts(),
|
fetchAlerts(),
|
||||||
@@ -132,6 +144,7 @@ export function App() {
|
|||||||
agents={agents}
|
agents={agents}
|
||||||
selectedAgentId={selectedAgentId}
|
selectedAgentId={selectedAgentId}
|
||||||
onSelectAgent={setSelectedAgentId}
|
onSelectAgent={setSelectedAgentId}
|
||||||
|
onRevokeAgent={onRevokeAgent}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ export type RevokeEnrollTokenResponse = {
|
|||||||
revoked: boolean;
|
revoked: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RevokeAgentResponse = {
|
||||||
|
agent_id: string;
|
||||||
|
revoked: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const res = await fetch(path, {
|
const res = await fetch(path, {
|
||||||
...init,
|
...init,
|
||||||
@@ -123,6 +128,13 @@ export function revokeEnrollToken(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function revokeAgent(agentId: string): Promise<RevokeAgentResponse> {
|
||||||
|
return request<RevokeAgentResponse>(
|
||||||
|
`/api/v1/control/agents/${encodeURIComponent(agentId)}`,
|
||||||
|
{ method: "DELETE" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function runCommand(
|
export function runCommand(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
kind: CommandKind,
|
kind: CommandKind,
|
||||||
|
|||||||
@@ -1,35 +1,79 @@
|
|||||||
import type { Agent } from "@/api";
|
import type { Agent } from "@/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
agents: Agent[];
|
agents: Agent[];
|
||||||
selectedAgentId: string;
|
selectedAgentId: string;
|
||||||
onSelectAgent: (agentId: string) => void;
|
onSelectAgent: (agentId: string) => void;
|
||||||
|
onRevokeAgent: (agentId: string) => Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AgentsPage({ agents, selectedAgentId, onSelectAgent }: Props) {
|
export function AgentsPage({
|
||||||
|
agents,
|
||||||
|
selectedAgentId,
|
||||||
|
onSelectAgent,
|
||||||
|
onRevokeAgent,
|
||||||
|
}: Props) {
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [status, setStatus] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
async function onRevoke(agentId: string) {
|
||||||
|
if (!window.confirm(`Revoke agent credentials for ${agentId}?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setBusy(true);
|
||||||
|
setStatus("");
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const revoked = await onRevokeAgent(agentId);
|
||||||
|
setStatus(
|
||||||
|
revoked ? `Revoked ${agentId}` : `${agentId} was already absent`,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
setError(String(err));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Agents</CardTitle>
|
<CardTitle>Agents</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="space-y-3">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
{agents.map((agent) => (
|
{agents.map((agent) => (
|
||||||
<Button
|
<div
|
||||||
|
className="flex items-start justify-between gap-3 rounded-md border bg-card px-3 py-2"
|
||||||
key={agent.agent_id}
|
key={agent.agent_id}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
variant={
|
variant={
|
||||||
selectedAgentId === agent.agent_id ? "secondary" : "outline"
|
selectedAgentId === agent.agent_id ? "secondary" : "outline"
|
||||||
}
|
}
|
||||||
className="flex h-auto w-full items-center justify-between px-3 py-2 text-left"
|
className="flex h-auto flex-1 items-center justify-between px-3 py-2 text-left"
|
||||||
onClick={() => onSelectAgent(agent.agent_id)}
|
onClick={() => onSelectAgent(agent.agent_id)}
|
||||||
|
disabled={busy}
|
||||||
>
|
>
|
||||||
<span>{agent.agent_id}</span>
|
<span>{agent.agent_id}</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{agent.connected ? "connected" : "offline"}
|
{agent.connected ? "connected" : "offline"}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="shrink-0 border-destructive/50 text-destructive hover:bg-destructive/10"
|
||||||
|
onClick={() => void onRevoke(agent.agent_id)}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
Revoke
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
{!agents.length && (
|
{!agents.length && (
|
||||||
<div className="px-1 py-2 text-sm text-muted-foreground">
|
<div className="px-1 py-2 text-sm text-muted-foreground">
|
||||||
@@ -37,6 +81,16 @@ export function AgentsPage({ agents, selectedAgentId, onSelectAgent }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{status && (
|
||||||
|
<pre className="max-h-80 overflow-auto rounded-md border bg-muted/40 p-3 text-xs">
|
||||||
|
{status}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
{error && (
|
||||||
|
<pre className="max-h-80 overflow-auto rounded-md border border-destructive/60 bg-destructive/10 p-3 text-xs text-destructive">
|
||||||
|
{error}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ pub struct RevokeEnrollTokenResponse {
|
|||||||
pub revoked: bool,
|
pub revoked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct RevokeAgentResponse {
|
||||||
|
pub agent_id: String,
|
||||||
|
pub revoked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct StateStatsResponse {
|
pub struct StateStatsResponse {
|
||||||
pub db_path: String,
|
pub db_path: String,
|
||||||
@@ -275,6 +281,56 @@ pub async fn revoke_enroll_token(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn revoke_agent(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
AxumPath(agent_id): AxumPath<String>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
match state.store.revoke_agent(&agent_id).await {
|
||||||
|
Ok(revoked) => {
|
||||||
|
if revoked {
|
||||||
|
// Remove any active session so this credential is also cut off at runtime.
|
||||||
|
state.sessions.write().await.remove(&agent_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(err) = state
|
||||||
|
.store
|
||||||
|
.append_audit_event(AuditEventInput {
|
||||||
|
actor_type: "admin_api".into(),
|
||||||
|
actor_id: None,
|
||||||
|
agent_id: Some(agent_id.clone()),
|
||||||
|
request_id: None,
|
||||||
|
event_type: "agent_revoke".into(),
|
||||||
|
outcome: if revoked {
|
||||||
|
"ok".into()
|
||||||
|
} else {
|
||||||
|
"not_found".into()
|
||||||
|
},
|
||||||
|
latency_ms: None,
|
||||||
|
message: if revoked {
|
||||||
|
"revoked agent credentials".into()
|
||||||
|
} else {
|
||||||
|
"agent credentials not found".into()
|
||||||
|
},
|
||||||
|
metadata: serde_json::json!({ "agent_id": agent_id }),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!(error = %err, "failed to append audit event for agent revoke");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((StatusCode::OK, Json(RevokeAgentResponse { agent_id, revoked })))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "failed to revoke agent credentials");
|
||||||
|
Err(json_error(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"revoke_agent_failed",
|
||||||
|
&err.to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn state_stats(
|
pub async fn state_stats(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ pub use alerts::{active_alerts, alert_history, alerts_stream};
|
|||||||
pub use audit::list_audit_events;
|
pub use audit::list_audit_events;
|
||||||
pub use commands::{list_agents, run_command};
|
pub use commands::{list_agents, run_command};
|
||||||
pub use control::{
|
pub use control::{
|
||||||
EnrollTokenStatus, IssueEnrollTokenResponse, RevokeEnrollTokenResponse, StateStatsResponse,
|
EnrollTokenStatus, IssueEnrollTokenResponse, RevokeAgentResponse, RevokeEnrollTokenResponse,
|
||||||
enroll, healthz, issue_enroll_token, list_enroll_tokens, revoke_enroll_token, state_stats,
|
StateStatsResponse, enroll, healthz, issue_enroll_token, list_enroll_tokens, revoke_agent,
|
||||||
|
revoke_enroll_token, state_stats,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn json_error(
|
pub fn json_error(
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ pub enum Command {
|
|||||||
ListEnrollTokens(ListEnrollTokensArgs),
|
ListEnrollTokens(ListEnrollTokensArgs),
|
||||||
/// Revoke a specific enroll token.
|
/// Revoke a specific enroll token.
|
||||||
RevokeEnrollToken(RevokeEnrollTokenArgs),
|
RevokeEnrollToken(RevokeEnrollTokenArgs),
|
||||||
|
/// Revoke an enrolled agent's persistent credentials.
|
||||||
|
RevokeAgent(RevokeAgentArgs),
|
||||||
/// Print state backend stats.
|
/// Print state backend stats.
|
||||||
StateStats(StateStatsArgs),
|
StateStats(StateStatsArgs),
|
||||||
/// Send SIGHUP to an already-running daemon.
|
/// Send SIGHUP to an already-running daemon.
|
||||||
@@ -189,6 +191,27 @@ pub struct RevokeEnrollTokenArgs {
|
|||||||
pub target: AdminTargetArgs,
|
pub target: AdminTargetArgs,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
pub struct RevokeAgentArgs {
|
||||||
|
#[arg(long, default_value = DEFAULT_CONFIG_FILE)]
|
||||||
|
pub config_file: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub state_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub data_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub public_url: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub agent_id: String,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub target: AdminTargetArgs,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
pub struct StateStatsArgs {
|
pub struct StateStatsArgs {
|
||||||
#[arg(long, default_value = DEFAULT_CONFIG_FILE)]
|
#[arg(long, default_value = DEFAULT_CONFIG_FILE)]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ mod types;
|
|||||||
pub use init::{bootstrap_config_if_missing, write_init_config};
|
pub use init::{bootstrap_config_if_missing, write_init_config};
|
||||||
pub use resolve::{
|
pub use resolve::{
|
||||||
issue_token_endpoint, resolve_issue_token_settings, resolve_list_enroll_token_settings,
|
issue_token_endpoint, resolve_issue_token_settings, resolve_list_enroll_token_settings,
|
||||||
resolve_revoke_enroll_token_settings, resolve_state_stats_settings,
|
resolve_revoke_agent_settings, resolve_revoke_enroll_token_settings,
|
||||||
|
resolve_state_stats_settings,
|
||||||
};
|
};
|
||||||
pub use types::{DaemonConfig, TelemetryConfig};
|
pub use types::{DaemonConfig, TelemetryConfig};
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use std::time::Duration;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
use crate::cli::{
|
use crate::cli::{
|
||||||
AdminTargetArgs, IssueEnrollTokenArgs, ListEnrollTokensArgs, RevokeEnrollTokenArgs, ServeArgs,
|
AdminTargetArgs, IssueEnrollTokenArgs, ListEnrollTokensArgs, RevokeAgentArgs,
|
||||||
StateStatsArgs,
|
RevokeEnrollTokenArgs, ServeArgs, StateStatsArgs,
|
||||||
};
|
};
|
||||||
use crate::config::types::{
|
use crate::config::types::{
|
||||||
DaemonConfig, FileConfig, FileTelemetryConfig, IssueTokenSettings, StateAccessSettings,
|
DaemonConfig, FileConfig, FileTelemetryConfig, IssueTokenSettings, StateAccessSettings,
|
||||||
@@ -163,6 +163,16 @@ pub fn resolve_revoke_enroll_token_settings(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn resolve_revoke_agent_settings(args: &RevokeAgentArgs) -> Result<StateAccessSettings> {
|
||||||
|
resolve_state_access(
|
||||||
|
&args.config_file,
|
||||||
|
args.data_dir.clone(),
|
||||||
|
args.state_file.clone(),
|
||||||
|
args.public_url.clone(),
|
||||||
|
&args.target,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resolve_state_stats_settings(args: &StateStatsArgs) -> Result<StateAccessSettings> {
|
pub fn resolve_state_stats_settings(args: &StateStatsArgs) -> Result<StateAccessSettings> {
|
||||||
resolve_state_access(
|
resolve_state_access(
|
||||||
&args.config_file,
|
&args.config_file,
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ async fn main() -> Result<()> {
|
|||||||
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
||||||
runtime::revoke_enroll_token(args).await
|
runtime::revoke_enroll_token(args).await
|
||||||
}
|
}
|
||||||
|
Command::RevokeAgent(args) => {
|
||||||
|
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
||||||
|
runtime::revoke_agent(args).await
|
||||||
|
}
|
||||||
Command::StateStats(args) => {
|
Command::StateStats(args) => {
|
||||||
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
tracing::init(cli.verbose, &config::TelemetryConfig::default())?;
|
||||||
runtime::state_stats(args).await
|
runtime::state_stats(args).await
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ use anyhow::{Context, Result};
|
|||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::cli::{
|
use crate::cli::{
|
||||||
IssueEnrollTokenArgs, ListEnrollTokensArgs, RevokeEnrollTokenArgs, StateStatsArgs,
|
IssueEnrollTokenArgs, ListEnrollTokensArgs, RevokeAgentArgs, RevokeEnrollTokenArgs,
|
||||||
|
StateStatsArgs,
|
||||||
};
|
};
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::state;
|
use crate::state;
|
||||||
@@ -174,6 +175,46 @@ pub async fn revoke_enroll_token(args: RevokeEnrollTokenArgs) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn revoke_agent(args: RevokeAgentArgs) -> Result<()> {
|
||||||
|
let settings = config::resolve_revoke_agent_settings(&args)?;
|
||||||
|
if let Some(base) = settings.public_url.as_deref() {
|
||||||
|
let url = format!("{}/api/v1/control/agents/{}", base, args.agent_id);
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let response = client
|
||||||
|
.delete(&url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("failed to call {url}"))?;
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status();
|
||||||
|
let body = response
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| "<unreadable error body>".to_string());
|
||||||
|
anyhow::bail!("live revoke-agent failed with {status}: {body}");
|
||||||
|
}
|
||||||
|
let body: api::RevokeAgentResponse = response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.context("failed to decode revoke-agent response")?;
|
||||||
|
println!("agent_id={} revoked={}", body.agent_id, body.revoked);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let store =
|
||||||
|
state::Store::load_or_init(&settings.state_file, Vec::new(), Duration::from_secs(1))
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"failed to initialize store {}",
|
||||||
|
settings.state_file.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let removed = store.revoke_agent(&args.agent_id).await?;
|
||||||
|
println!("agent_id={} revoked={}", args.agent_id, removed);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn state_stats(args: StateStatsArgs) -> Result<()> {
|
pub async fn state_stats(args: StateStatsArgs) -> Result<()> {
|
||||||
let settings = config::resolve_state_stats_settings(&args)?;
|
let settings = config::resolve_state_stats_settings(&args)?;
|
||||||
if let Some(base) = settings.public_url.as_deref() {
|
if let Some(base) = settings.public_url.as_deref() {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ use crate::ws;
|
|||||||
mod admin;
|
mod admin;
|
||||||
mod process;
|
mod process;
|
||||||
pub use admin::{issue_enroll_token, list_enroll_tokens, revoke_enroll_token, state_stats};
|
pub use admin::{issue_enroll_token, list_enroll_tokens, revoke_enroll_token, state_stats};
|
||||||
|
pub use admin::revoke_agent;
|
||||||
pub use process::reload_daemon;
|
pub use process::reload_daemon;
|
||||||
use process::{remove_pid_file, write_pid_file};
|
use process::{remove_pid_file, write_pid_file};
|
||||||
|
|
||||||
@@ -85,6 +86,10 @@ fn control_api_routes() -> Router<AppState> {
|
|||||||
.route("/api/v1/control/alerts/history", get(api::alert_history))
|
.route("/api/v1/control/alerts/history", get(api::alert_history))
|
||||||
.route("/api/v1/control/alerts/ws", get(api::alerts_stream))
|
.route("/api/v1/control/alerts/ws", get(api::alerts_stream))
|
||||||
.route("/api/v1/control/agents", get(api::list_agents))
|
.route("/api/v1/control/agents", get(api::list_agents))
|
||||||
|
.route(
|
||||||
|
"/api/v1/control/agents/{agent_id}",
|
||||||
|
axum::routing::delete(api::revoke_agent),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/control/agents/{agent_id}/command",
|
"/api/v1/control/agents/{agent_id}/command",
|
||||||
post(api::run_command),
|
post(api::run_command),
|
||||||
|
|||||||
@@ -187,6 +187,19 @@ impl Store {
|
|||||||
Ok(removed)
|
Ok(removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn revoke_agent(&self, agent_id: &str) -> Result<bool> {
|
||||||
|
let removed = self
|
||||||
|
.agents
|
||||||
|
.remove(agent_id.as_bytes())
|
||||||
|
.context("failed removing agent credentials")?
|
||||||
|
.is_some();
|
||||||
|
if removed {
|
||||||
|
self.flush()
|
||||||
|
.context("failed flushing db after agent revoke")?;
|
||||||
|
}
|
||||||
|
Ok(removed)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn stats(&self) -> Result<StateStats> {
|
pub async fn stats(&self) -> Result<StateStats> {
|
||||||
let now = now_unix();
|
let now = now_unix();
|
||||||
let mut enroll_token_count = 0usize;
|
let mut enroll_token_count = 0usize;
|
||||||
@@ -694,6 +707,46 @@ mod tests {
|
|||||||
cleanup_dir(&dir);
|
cleanup_dir(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn revoke_agent_removes_credentials() {
|
||||||
|
let (store, dir) = make_store().await;
|
||||||
|
|
||||||
|
store
|
||||||
|
.enroll_tokens
|
||||||
|
.insert(b"enr-revoke-agent-test", &(u64::MAX - 10).to_le_bytes())
|
||||||
|
.expect("insert should succeed");
|
||||||
|
|
||||||
|
let issued = store
|
||||||
|
.enroll("enr-revoke-agent-test")
|
||||||
|
.await
|
||||||
|
.expect("enroll should succeed");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
store
|
||||||
|
.verify_agent_token(&issued.agent_id, &issued.agent_token)
|
||||||
|
.await
|
||||||
|
);
|
||||||
|
|
||||||
|
let removed = store
|
||||||
|
.revoke_agent(&issued.agent_id)
|
||||||
|
.await
|
||||||
|
.expect("revoke should succeed");
|
||||||
|
assert!(removed);
|
||||||
|
assert!(
|
||||||
|
!store
|
||||||
|
.verify_agent_token(&issued.agent_id, &issued.agent_token)
|
||||||
|
.await
|
||||||
|
);
|
||||||
|
|
||||||
|
let removed_again = store
|
||||||
|
.revoke_agent(&issued.agent_id)
|
||||||
|
.await
|
||||||
|
.expect("second revoke should succeed");
|
||||||
|
assert!(!removed_again);
|
||||||
|
|
||||||
|
cleanup_dir(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn audit_events_append_and_filter() {
|
async fn audit_events_append_and_filter() {
|
||||||
let (store, dir) = make_store().await;
|
let (store, dir) = make_store().await;
|
||||||
|
|||||||
Reference in New Issue
Block a user