nickname
This commit is contained in:
@@ -196,6 +196,7 @@ Control-plane admin API includes token management endpoints:
|
|||||||
- `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}`
|
- `DELETE /api/v1/control/agents/{agent_id}`
|
||||||
|
- `PATCH /api/v1/control/agents/{agent_id}/nickname`
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
fetchAlerts,
|
fetchAlerts,
|
||||||
fetchAudit,
|
fetchAudit,
|
||||||
revokeAgent,
|
revokeAgent,
|
||||||
|
setAgentNickname,
|
||||||
} from "@/api";
|
} from "@/api";
|
||||||
import { AppLayout } from "@/layout/AppLayout";
|
import { AppLayout } from "@/layout/AppLayout";
|
||||||
import { AgentsPage } from "@/pages/AgentsPage";
|
import { AgentsPage } from "@/pages/AgentsPage";
|
||||||
@@ -73,6 +74,15 @@ export function App() {
|
|||||||
return result.revoked;
|
return result.revoked;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onSetAgentNickname(
|
||||||
|
agentId: string,
|
||||||
|
nickname: string | null,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const result = await setAgentNickname(agentId, nickname);
|
||||||
|
await loadAll();
|
||||||
|
return result.updated;
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshAlertsAndHistory() {
|
async function refreshAlertsAndHistory() {
|
||||||
const [nextAlerts, nextHistory] = await Promise.all([
|
const [nextAlerts, nextHistory] = await Promise.all([
|
||||||
fetchAlerts(),
|
fetchAlerts(),
|
||||||
@@ -149,6 +159,7 @@ export function App() {
|
|||||||
selectedAgentId={selectedAgentId}
|
selectedAgentId={selectedAgentId}
|
||||||
onSelectAgent={setSelectedAgentId}
|
onSelectAgent={setSelectedAgentId}
|
||||||
onRevokeAgent={onRevokeAgent}
|
onRevokeAgent={onRevokeAgent}
|
||||||
|
onSetAgentNickname={onSetAgentNickname}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+25
-1
@@ -1,4 +1,8 @@
|
|||||||
export type Agent = { agent_id: string; connected: boolean };
|
export type Agent = {
|
||||||
|
agent_id: string;
|
||||||
|
connected: boolean;
|
||||||
|
nickname?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type Alert = {
|
export type Alert = {
|
||||||
alert_id: string;
|
alert_id: string;
|
||||||
@@ -62,6 +66,12 @@ export type RevokeAgentResponse = {
|
|||||||
revoked: boolean;
|
revoked: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SetAgentNicknameResponse = {
|
||||||
|
agent_id: string;
|
||||||
|
nickname: string | null;
|
||||||
|
updated: 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,
|
||||||
@@ -134,6 +144,20 @@ export function revokeAgent(agentId: string): Promise<RevokeAgentResponse> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setAgentNickname(
|
||||||
|
agentId: string,
|
||||||
|
nickname: string | null,
|
||||||
|
): Promise<SetAgentNicknameResponse> {
|
||||||
|
const normalized = nickname?.trim() ?? "";
|
||||||
|
return request<SetAgentNicknameResponse>(
|
||||||
|
`/api/v1/control/agents/${encodeURIComponent(agentId)}/nickname`,
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ nickname: normalized ? normalized : null }),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function runCommand(
|
export function runCommand(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
kind: CommandKind,
|
kind: CommandKind,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 { Input } from "@/components/ui/input";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -8,6 +9,10 @@ type Props = {
|
|||||||
selectedAgentId: string;
|
selectedAgentId: string;
|
||||||
onSelectAgent: (agentId: string) => void;
|
onSelectAgent: (agentId: string) => void;
|
||||||
onRevokeAgent: (agentId: string) => Promise<boolean>;
|
onRevokeAgent: (agentId: string) => Promise<boolean>;
|
||||||
|
onSetAgentNickname: (
|
||||||
|
agentId: string,
|
||||||
|
nickname: string | null,
|
||||||
|
) => Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AgentsPage({
|
export function AgentsPage({
|
||||||
@@ -15,10 +20,17 @@ export function AgentsPage({
|
|||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
onSelectAgent,
|
onSelectAgent,
|
||||||
onRevokeAgent,
|
onRevokeAgent,
|
||||||
|
onSetAgentNickname,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [status, setStatus] = useState("");
|
const [status, setStatus] = useState("");
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [drafts, setDrafts] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
function displayLabel(agent: Agent): string {
|
||||||
|
const nickname = agent.nickname?.trim();
|
||||||
|
return nickname ? nickname : agent.agent_id;
|
||||||
|
}
|
||||||
|
|
||||||
async function onRevoke(agentId: string) {
|
async function onRevoke(agentId: string) {
|
||||||
if (!window.confirm(`Revoke agent credentials for ${agentId}?`)) {
|
if (!window.confirm(`Revoke agent credentials for ${agentId}?`)) {
|
||||||
@@ -39,6 +51,25 @@ export function AgentsPage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onSaveNickname(agent: Agent) {
|
||||||
|
const draft = drafts[agent.agent_id] ?? agent.nickname ?? "";
|
||||||
|
setBusy(true);
|
||||||
|
setStatus("");
|
||||||
|
setError("");
|
||||||
|
try {
|
||||||
|
const updated = await onSetAgentNickname(agent.agent_id, draft);
|
||||||
|
setStatus(
|
||||||
|
updated
|
||||||
|
? `Updated nickname for ${agent.agent_id}`
|
||||||
|
: `${agent.agent_id} not found`,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
setError(String(err));
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -55,15 +86,36 @@ export function AgentsPage({
|
|||||||
variant={
|
variant={
|
||||||
selectedAgentId === agent.agent_id ? "secondary" : "outline"
|
selectedAgentId === agent.agent_id ? "secondary" : "outline"
|
||||||
}
|
}
|
||||||
className="flex h-auto flex-1 items-center justify-between px-3 py-2 text-left"
|
className="flex h-auto flex-1 items-center justify-between gap-3 px-3 py-2 text-left"
|
||||||
onClick={() => onSelectAgent(agent.agent_id)}
|
onClick={() => onSelectAgent(agent.agent_id)}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
>
|
>
|
||||||
<span>{agent.agent_id}</span>
|
<span className="min-w-0 truncate">{displayLabel(agent)}</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>
|
||||||
|
<div className="grid min-w-52 gap-2">
|
||||||
|
<Input
|
||||||
|
value={drafts[agent.agent_id] ?? agent.nickname ?? ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDrafts((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[agent.agent_id]: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder="nickname (optional)"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => void onSaveNickname(agent)}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
Save Name
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -74,6 +126,8 @@ export function AgentsPage({
|
|||||||
Revoke
|
Revoke
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ export function CommandsPage({
|
|||||||
onSelectAgent,
|
onSelectAgent,
|
||||||
onAfterCommand,
|
onAfterCommand,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
function displayAgentLabel(agent: Agent): string {
|
||||||
|
const nickname = agent.nickname?.trim();
|
||||||
|
return nickname ? nickname : agent.agent_id;
|
||||||
|
}
|
||||||
|
|
||||||
const [kind, setKind] = useState<CommandKind>("devs");
|
const [kind, setKind] = useState<CommandKind>("devs");
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [output, setOutput] = useState("Select agent and run a command");
|
const [output, setOutput] = useState("Select agent and run a command");
|
||||||
@@ -77,7 +82,9 @@ export function CommandsPage({
|
|||||||
className={`size-2 shrink-0 rounded-full ${agent.connected ? "bg-emerald-500" : "bg-zinc-400"}`}
|
className={`size-2 shrink-0 rounded-full ${agent.connected ? "bg-emerald-500" : "bg-zinc-400"}`}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0 truncate">{agent.agent_id}</span>
|
<span className="min-w-0 truncate">
|
||||||
|
{displayAgentLabel(agent)}
|
||||||
|
</span>
|
||||||
<span className="shrink-0 text-xs text-muted-foreground">
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
{agent.connected ? "connected" : "offline"}
|
{agent.connected ? "connected" : "offline"}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ export function DevicesPage({
|
|||||||
onSelectAgent,
|
onSelectAgent,
|
||||||
onAfterWake,
|
onAfterWake,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
function displayAgentLabel(agent: Agent): string {
|
||||||
|
const nickname = agent.nickname?.trim();
|
||||||
|
return nickname ? nickname : agent.agent_id;
|
||||||
|
}
|
||||||
|
|
||||||
const selectedAgent = agents.find(
|
const selectedAgent = agents.find(
|
||||||
(agent) => agent.agent_id === selectedAgentId,
|
(agent) => agent.agent_id === selectedAgentId,
|
||||||
);
|
);
|
||||||
@@ -351,7 +356,9 @@ export function DevicesPage({
|
|||||||
) : null}
|
) : null}
|
||||||
<span className="min-w-0 truncate">
|
<span className="min-w-0 truncate">
|
||||||
{selectedAgentId
|
{selectedAgentId
|
||||||
? selectedAgentId
|
? selectedAgent
|
||||||
|
? displayAgentLabel(selectedAgent)
|
||||||
|
: selectedAgentId
|
||||||
: "Select connected agent"}
|
: "Select connected agent"}
|
||||||
</span>
|
</span>
|
||||||
{selectedAgent ? (
|
{selectedAgent ? (
|
||||||
@@ -378,7 +385,7 @@ export function DevicesPage({
|
|||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<span className="min-w-0 truncate">
|
<span className="min-w-0 truncate">
|
||||||
{agent.agent_id}
|
{displayAgentLabel(agent)}
|
||||||
</span>
|
</span>
|
||||||
<span className="shrink-0 text-xs text-muted-foreground">
|
<span className="shrink-0 text-xs text-muted-foreground">
|
||||||
{agent.connected ? "connected" : "offline"}
|
{agent.connected ? "connected" : "offline"}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use crate::state::AuditEventInput;
|
|||||||
pub struct AgentStatus {
|
pub struct AgentStatus {
|
||||||
pub agent_id: String,
|
pub agent_id: String,
|
||||||
pub connected: bool,
|
pub connected: bool,
|
||||||
|
pub nickname: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -37,14 +38,15 @@ pub struct RelayCommandResponse {
|
|||||||
pub async fn list_agents(
|
pub async fn list_agents(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let enrolled = state.store.list_agents().await;
|
let enrolled = state.store.list_agents_with_nicknames().await;
|
||||||
let sessions = state.sessions.read().await;
|
let sessions = state.sessions.read().await;
|
||||||
|
|
||||||
let agents = enrolled
|
let agents = enrolled
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|agent_id| AgentStatus {
|
.map(|(agent_id, nickname)| AgentStatus {
|
||||||
connected: sessions.contains_key(&agent_id),
|
connected: sessions.contains_key(&agent_id),
|
||||||
agent_id,
|
agent_id,
|
||||||
|
nickname,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,18 @@ pub struct RevokeAgentResponse {
|
|||||||
pub revoked: bool,
|
pub revoked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct SetAgentNicknameRequest {
|
||||||
|
pub nickname: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SetAgentNicknameResponse {
|
||||||
|
pub agent_id: String,
|
||||||
|
pub nickname: Option<String>,
|
||||||
|
pub updated: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct StateStatsResponse {
|
pub struct StateStatsResponse {
|
||||||
pub db_path: String,
|
pub db_path: String,
|
||||||
@@ -336,6 +348,73 @@ pub async fn revoke_agent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_agent_nickname(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
AxumPath(agent_id): AxumPath<String>,
|
||||||
|
Json(req): Json<SetAgentNicknameRequest>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
|
||||||
|
let normalized = req
|
||||||
|
.nickname
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.map(ToOwned::to_owned);
|
||||||
|
|
||||||
|
match state
|
||||||
|
.store
|
||||||
|
.set_agent_nickname(&agent_id, normalized.as_deref())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(updated) => {
|
||||||
|
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_nickname_set".into(),
|
||||||
|
outcome: if updated {
|
||||||
|
"ok".into()
|
||||||
|
} else {
|
||||||
|
"not_found".into()
|
||||||
|
},
|
||||||
|
latency_ms: None,
|
||||||
|
message: if updated {
|
||||||
|
"updated agent nickname".into()
|
||||||
|
} else {
|
||||||
|
"agent not found for nickname update".into()
|
||||||
|
},
|
||||||
|
metadata: serde_json::json!({
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"nickname": normalized,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!(error = %err, "failed to append audit event for nickname set");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(SetAgentNicknameResponse {
|
||||||
|
agent_id,
|
||||||
|
nickname: normalized,
|
||||||
|
updated,
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(error = %err, "failed to update agent nickname");
|
||||||
|
Err(json_error(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"set_agent_nickname_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>)> {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub use commands::{list_agents, run_command};
|
|||||||
pub use control::{
|
pub use control::{
|
||||||
EnrollTokenStatus, IssueEnrollTokenResponse, RevokeAgentResponse, RevokeEnrollTokenResponse,
|
EnrollTokenStatus, IssueEnrollTokenResponse, RevokeAgentResponse, RevokeEnrollTokenResponse,
|
||||||
StateStatsResponse, enroll, healthz, issue_enroll_token, list_enroll_tokens, revoke_agent,
|
StateStatsResponse, enroll, healthz, issue_enroll_token, list_enroll_tokens, revoke_agent,
|
||||||
revoke_enroll_token, state_stats,
|
revoke_enroll_token, set_agent_nickname, state_stats,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn json_error(
|
pub fn json_error(
|
||||||
|
|||||||
@@ -95,6 +95,10 @@ fn control_api_routes() -> Router<AppState> {
|
|||||||
"/api/v1/control/agents/{agent_id}",
|
"/api/v1/control/agents/{agent_id}",
|
||||||
axum::routing::delete(api::revoke_agent),
|
axum::routing::delete(api::revoke_agent),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/v1/control/agents/{agent_id}/nickname",
|
||||||
|
axum::routing::patch(api::set_agent_nickname),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/v1/control/agents/{agent_id}/command",
|
"/api/v1/control/agents/{agent_id}/command",
|
||||||
post(api::run_command),
|
post(api::run_command),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ pub struct Store {
|
|||||||
meta: sled::Tree,
|
meta: sled::Tree,
|
||||||
enroll_tokens: sled::Tree,
|
enroll_tokens: sled::Tree,
|
||||||
agents: sled::Tree,
|
agents: sled::Tree,
|
||||||
|
agent_meta: sled::Tree,
|
||||||
audit_events: sled::Tree,
|
audit_events: sled::Tree,
|
||||||
active_alerts: sled::Tree,
|
active_alerts: sled::Tree,
|
||||||
alert_transitions: sled::Tree,
|
alert_transitions: sled::Tree,
|
||||||
@@ -45,6 +46,9 @@ impl Store {
|
|||||||
let agents_tree = db
|
let agents_tree = db
|
||||||
.open_tree("agents")
|
.open_tree("agents")
|
||||||
.context("failed to open agents tree")?;
|
.context("failed to open agents tree")?;
|
||||||
|
let agent_meta_tree = db
|
||||||
|
.open_tree("agent_meta")
|
||||||
|
.context("failed to open agent_meta tree")?;
|
||||||
let audit_events_tree = db
|
let audit_events_tree = db
|
||||||
.open_tree("audit_events")
|
.open_tree("audit_events")
|
||||||
.context("failed to open audit_events tree")?;
|
.context("failed to open audit_events tree")?;
|
||||||
@@ -60,6 +64,7 @@ impl Store {
|
|||||||
meta: meta_tree,
|
meta: meta_tree,
|
||||||
enroll_tokens: enroll_tree,
|
enroll_tokens: enroll_tree,
|
||||||
agents: agents_tree,
|
agents: agents_tree,
|
||||||
|
agent_meta: agent_meta_tree,
|
||||||
audit_events: audit_events_tree,
|
audit_events: audit_events_tree,
|
||||||
active_alerts: active_alerts_tree,
|
active_alerts: active_alerts_tree,
|
||||||
alert_transitions: alert_transitions_tree,
|
alert_transitions: alert_transitions_tree,
|
||||||
@@ -194,12 +199,39 @@ impl Store {
|
|||||||
.context("failed removing agent credentials")?
|
.context("failed removing agent credentials")?
|
||||||
.is_some();
|
.is_some();
|
||||||
if removed {
|
if removed {
|
||||||
|
let _ = self.agent_meta.remove(agent_id.as_bytes());
|
||||||
self.flush()
|
self.flush()
|
||||||
.context("failed flushing db after agent revoke")?;
|
.context("failed flushing db after agent revoke")?;
|
||||||
}
|
}
|
||||||
Ok(removed)
|
Ok(removed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn set_agent_nickname(&self, agent_id: &str, nickname: Option<&str>) -> Result<bool> {
|
||||||
|
if !self
|
||||||
|
.agents
|
||||||
|
.contains_key(agent_id.as_bytes())
|
||||||
|
.context("failed checking agent existence")?
|
||||||
|
{
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = nickname.map(str::trim).filter(|v| !v.is_empty());
|
||||||
|
if let Some(value) = normalized {
|
||||||
|
self.agent_meta
|
||||||
|
.insert(agent_id.as_bytes(), value.as_bytes())
|
||||||
|
.context("failed persisting agent nickname")?;
|
||||||
|
} else {
|
||||||
|
let _ = self
|
||||||
|
.agent_meta
|
||||||
|
.remove(agent_id.as_bytes())
|
||||||
|
.context("failed clearing agent nickname")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.flush()
|
||||||
|
.context("failed flushing db after nickname update")?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -257,6 +289,28 @@ impl Store {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_agents_with_nicknames(&self) -> Vec<(String, Option<String>)> {
|
||||||
|
let mut out = self
|
||||||
|
.agents
|
||||||
|
.iter()
|
||||||
|
.filter_map(|item| item.ok())
|
||||||
|
.filter_map(|(key, _)| String::from_utf8(key.to_vec()).ok())
|
||||||
|
.map(|agent_id| {
|
||||||
|
let nickname = self
|
||||||
|
.agent_meta
|
||||||
|
.get(agent_id.as_bytes())
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.and_then(|v| String::from_utf8(v.to_vec()).ok())
|
||||||
|
.map(|v| v.trim().to_string())
|
||||||
|
.filter(|v| !v.is_empty());
|
||||||
|
(agent_id, nickname)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
out.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn append_audit_event(&self, input: AuditEventInput) -> Result<AuditEvent> {
|
pub async fn append_audit_event(&self, input: AuditEventInput) -> Result<AuditEvent> {
|
||||||
let event = AuditEvent {
|
let event = AuditEvent {
|
||||||
event_id: format!("evt-{}", Uuid::new_v4()),
|
event_id: format!("evt-{}", Uuid::new_v4()),
|
||||||
@@ -417,6 +471,9 @@ impl Store {
|
|||||||
.flush()
|
.flush()
|
||||||
.context("failed to flush enroll token tree")?;
|
.context("failed to flush enroll token tree")?;
|
||||||
self.agents.flush().context("failed to flush agents tree")?;
|
self.agents.flush().context("failed to flush agents tree")?;
|
||||||
|
self.agent_meta
|
||||||
|
.flush()
|
||||||
|
.context("failed to flush agent_meta tree")?;
|
||||||
self.audit_events
|
self.audit_events
|
||||||
.flush()
|
.flush()
|
||||||
.context("failed to flush audit event tree")?;
|
.context("failed to flush audit event tree")?;
|
||||||
@@ -747,6 +804,45 @@ mod tests {
|
|||||||
cleanup_dir(&dir);
|
cleanup_dir(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn nickname_set_and_clear_roundtrip() {
|
||||||
|
let (store, dir) = make_store().await;
|
||||||
|
|
||||||
|
store
|
||||||
|
.enroll_tokens
|
||||||
|
.insert(b"enr-nickname-test", &(u64::MAX - 10).to_le_bytes())
|
||||||
|
.expect("insert should succeed");
|
||||||
|
|
||||||
|
let issued = store
|
||||||
|
.enroll("enr-nickname-test")
|
||||||
|
.await
|
||||||
|
.expect("enroll should succeed");
|
||||||
|
|
||||||
|
let updated = store
|
||||||
|
.set_agent_nickname(&issued.agent_id, Some("kitchen-router"))
|
||||||
|
.await
|
||||||
|
.expect("nickname set should succeed");
|
||||||
|
assert!(updated);
|
||||||
|
|
||||||
|
let listed = store.list_agents_with_nicknames().await;
|
||||||
|
assert!(listed.iter().any(|(id, name)| {
|
||||||
|
id == &issued.agent_id && name.as_deref() == Some("kitchen-router")
|
||||||
|
}));
|
||||||
|
|
||||||
|
let cleared = store
|
||||||
|
.set_agent_nickname(&issued.agent_id, None)
|
||||||
|
.await
|
||||||
|
.expect("nickname clear should succeed");
|
||||||
|
assert!(cleared);
|
||||||
|
|
||||||
|
let listed = store.list_agents_with_nicknames().await;
|
||||||
|
assert!(listed
|
||||||
|
.iter()
|
||||||
|
.any(|(id, name)| id == &issued.agent_id && name.is_none()));
|
||||||
|
|
||||||
|
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