feat: 添加结构化日志与审计
- tracing + tracing-subscriber,全局 --verbose/-v 与 RUST_LOG 控制 - 新增 audit_log 表,add/update/delete 成功后自动写入审计记录 - 新增 src/audit.rs,审计失败仅 warn 不中断主流程 - 更新 README/AGENTS.md,补充 verbose、audit_log 说明 - .vscode/tasks.json 增加 verbose/update/audit 测试任务 Made-with: Cursor
This commit is contained in:
44
.vscode/tasks.json
vendored
44
.vscode/tasks.json
vendored
@@ -25,12 +25,36 @@
|
||||
"command": "./target/debug/secrets help add",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "cli: help config",
|
||||
"type": "shell",
|
||||
"command": "./target/debug/secrets help config",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "cli: config path",
|
||||
"type": "shell",
|
||||
"command": "./target/debug/secrets config path",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "cli: config show",
|
||||
"type": "shell",
|
||||
"command": "./target/debug/secrets config show",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test: search all",
|
||||
"type": "shell",
|
||||
"command": "./target/debug/secrets search",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test: search all (verbose)",
|
||||
"type": "shell",
|
||||
"command": "./target/debug/secrets --verbose search",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test: search by namespace (refining)",
|
||||
"type": "shell",
|
||||
@@ -82,7 +106,7 @@
|
||||
{
|
||||
"label": "test: search with secrets revealed",
|
||||
"type": "shell",
|
||||
"command": "./target/debug/secrets search -n refining --kind service --name gitea --show-secrets",
|
||||
"command": "./target/debug/secrets search -n refining --kind service --show-secrets",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
@@ -97,6 +121,24 @@
|
||||
"command": "echo '--- add ---' && ./target/debug/secrets add -n test --kind demo --name roundtrip-test --tag test -m foo=bar -s password=secret123 && echo '--- search ---' && ./target/debug/secrets search -n test --show-secrets && echo '--- delete ---' && ./target/debug/secrets delete -n test --kind demo --name roundtrip-test && echo '--- verify deleted ---' && ./target/debug/secrets search -n test",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test: add + delete roundtrip (verbose)",
|
||||
"type": "shell",
|
||||
"command": "echo '--- add (verbose) ---' && ./target/debug/secrets --verbose add -n test --kind demo --name roundtrip-verbose --tag test -m foo=bar -s password=secret123 && echo '--- delete (verbose) ---' && ./target/debug/secrets --verbose delete -n test --kind demo --name roundtrip-verbose",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test: update roundtrip",
|
||||
"type": "shell",
|
||||
"command": "echo '--- add ---' && ./target/debug/secrets add -n test --kind demo --name update-test --tag v1 -m env=staging && echo '--- update ---' && ./target/debug/secrets update -n test --kind demo --name update-test --add-tag v2 --remove-tag v1 -m env=production && echo '--- verify ---' && ./target/debug/secrets search -n test --kind demo && echo '--- cleanup ---' && ./target/debug/secrets delete -n test --kind demo --name update-test",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test: audit log",
|
||||
"type": "shell",
|
||||
"command": "echo '--- add ---' && ./target/debug/secrets add -n test --kind demo --name audit-test -m foo=bar -s key=val && echo '--- update ---' && ./target/debug/secrets update -n test --kind demo --name audit-test -m foo=baz && echo '--- delete ---' && ./target/debug/secrets delete -n test --kind demo --name audit-test && echo '--- audit log (last 5) ---' && psql $DATABASE_URL -c \"SELECT action, namespace, kind, name, actor, detail, created_at FROM audit_log ORDER BY created_at DESC LIMIT 5;\"",
|
||||
"dependsOn": "build"
|
||||
},
|
||||
{
|
||||
"label": "test: add with file secret",
|
||||
"type": "shell",
|
||||
|
||||
59
AGENTS.md
59
AGENTS.md
@@ -7,19 +7,22 @@
|
||||
```
|
||||
secrets/
|
||||
src/
|
||||
main.rs # CLI 入口,clap 命令定义,auto-migrate
|
||||
db.rs # PgPool 创建 + 建表/索引(幂等)
|
||||
main.rs # CLI 入口,clap 命令定义,auto-migrate,--verbose 全局参数
|
||||
config.rs # 配置读写:~/.config/secrets/config.toml(database_url)
|
||||
db.rs # PgPool 创建 + 建表/索引(幂等,含 audit_log)
|
||||
models.rs # Secret 结构体(sqlx::FromRow + serde)
|
||||
audit.rs # 审计写入:向 audit_log 表记录所有写操作
|
||||
commands/
|
||||
add.rs # add 命令:upsert,支持 --meta key=value / --secret key=@file
|
||||
config.rs # config 命令:set-db / show / path(持久化 database_url)
|
||||
search.rs # search 命令:多条件动态查询
|
||||
delete.rs # delete 命令
|
||||
update.rs # update 命令:增量更新(合并 tags/metadata/encrypted)
|
||||
scripts/
|
||||
seed-data.sh # 从 refining/ricnsmart config.toml 导入全量数据
|
||||
.gitea/workflows/
|
||||
secrets.yml # CI:fmt + clippy + musl 构建 + Release 上传 + 飞书通知
|
||||
.vscode/tasks.json # 本地测试任务(build / search / add+delete roundtrip 等)
|
||||
.env # DATABASE_URL(gitignore,不提交)
|
||||
.vscode/tasks.json # 本地测试任务(build / config / search / add+delete / update / audit 等)
|
||||
```
|
||||
|
||||
## 数据库
|
||||
@@ -27,7 +30,7 @@ secrets/
|
||||
- **Host**: `47.117.131.22:5432`(阿里云上海 ECS,PostgreSQL 18 with io_uring)
|
||||
- **Database**: `secrets`
|
||||
- **连接串**: `postgres://postgres:<password>@47.117.131.22:5432/secrets`
|
||||
- **表**: 单张 `secrets` 表,首次连接自动建表(auto-migrate)
|
||||
- **表**: `secrets`(主表)+ `audit_log`(审计表),首次连接自动建表(auto-migrate)
|
||||
|
||||
### 表结构
|
||||
|
||||
@@ -46,6 +49,21 @@ secrets (
|
||||
)
|
||||
```
|
||||
|
||||
### audit_log 表结构
|
||||
|
||||
```sql
|
||||
audit_log (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
action VARCHAR(32) NOT NULL, -- 'add' | 'update' | 'delete'
|
||||
namespace VARCHAR(64) NOT NULL,
|
||||
kind VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
detail JSONB NOT NULL DEFAULT '{}', -- 变更摘要(tags/meta keys/secret keys,不含 value)
|
||||
actor VARCHAR(128) NOT NULL DEFAULT '', -- 操作者($USER 环境变量)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
```
|
||||
|
||||
### 字段职责划分
|
||||
|
||||
| 字段 | 存什么 | 示例 |
|
||||
@@ -57,6 +75,18 @@ secrets (
|
||||
| `metadata` | 明文非敏感信息 | `{"ip":"47.243.154.187","desc":"Grafana","domains":["..."]}` |
|
||||
| `encrypted` | 敏感凭据(MVP 阶段明文存储,后续对 value 加密) | `{"ssh_key":"-----BEGIN...","password":"..."}` |
|
||||
|
||||
## 数据库配置
|
||||
|
||||
首次使用需显式配置数据库连接,设置一次后在该设备上持久生效:
|
||||
|
||||
```bash
|
||||
secrets config set-db "postgres://postgres:<password>@47.117.131.22:5432/secrets"
|
||||
secrets config show # 查看当前配置(密码脱敏)
|
||||
secrets config path # 打印配置文件路径
|
||||
```
|
||||
|
||||
配置文件:`~/.config/secrets/config.toml`,权限 0600。`--db-url` 参数可一次性覆盖。
|
||||
|
||||
## CLI 命令
|
||||
|
||||
```bash
|
||||
@@ -77,6 +107,11 @@ secrets add -n <namespace> --kind <kind> --name <name> \
|
||||
secrets search [-n <namespace>] [--kind <kind>] [--tag <tag>] [-q <keyword>] [--show-secrets]
|
||||
# -q 匹配范围:name、namespace、kind、metadata 全文内容、tags
|
||||
|
||||
# 开启 debug 级别日志(全局参数,位于子命令之前)
|
||||
secrets --verbose <subcommand>
|
||||
secrets -v <subcommand>
|
||||
# 或通过环境变量控制:RUST_LOG=secrets=trace secrets search
|
||||
|
||||
# 增量更新已有记录(合并语义,记录不存在则报错)
|
||||
secrets update -n <namespace> --kind <kind> --name <name> \
|
||||
[--add-tag <tag>]... # 添加标签(不影响已有标签)
|
||||
@@ -88,6 +123,11 @@ secrets update -n <namespace> --kind <kind> --name <name> \
|
||||
|
||||
# 删除
|
||||
secrets delete -n <namespace> --kind <kind> --name <name>
|
||||
|
||||
# 配置(持久化 database_url,设置一次即可)
|
||||
secrets config set-db <url>
|
||||
secrets config show
|
||||
secrets config path
|
||||
```
|
||||
|
||||
### 示例
|
||||
@@ -134,7 +174,9 @@ secrets update -n refining --kind service --name mqtt \
|
||||
- 异步:全程 `tokio`,数据库操作 `sqlx` async
|
||||
- SQL:使用 `sqlx::query` / `sqlx::query_as` 绑定参数,禁止字符串拼接(搜索的动态 WHERE 子句除外,需使用参数绑定 `$1/$2`)
|
||||
- 新增 `kind` 类型时:只需在 `add` 调用时传入,无需改代码
|
||||
- 字段命名:CLI 短标志 `-n`=namespace,`-m`=meta,`-s`=secret,`-q`=query
|
||||
- 字段命名:CLI 短标志 `-n`=namespace,`-m`=meta,`-s`=secret,`-q`=query,`-v`=verbose
|
||||
- 日志:用户可见输出用 `println!`;调试/运维信息用 `tracing::debug!`/`info!`/`warn!`/`error!`
|
||||
- 审计:`add`/`update`/`delete` 成功后调用 `audit::log()`,写入 `audit_log` 表;失败只 warn 不中断
|
||||
|
||||
## 提交前检查(必须全部通过)
|
||||
|
||||
@@ -181,4 +223,7 @@ cargo fmt -- --check && cargo clippy -- -D warnings && cargo test
|
||||
|
||||
| 变量 | 说明 |
|
||||
|------|------|
|
||||
| `DATABASE_URL` | PostgreSQL 连接串,优先级高于 `--db-url` 参数 |
|
||||
| `RUST_LOG` | 日志级别,如 `secrets=debug`、`secrets=trace`(默认 warn) |
|
||||
| `USER` | 审计日志 actor 字段来源,Shell 自动设置,通常无需手动配置 |
|
||||
|
||||
数据库连接通过 `secrets config set-db` 持久化到 `~/.config/secrets/config.toml`,不支持环境变量。
|
||||
|
||||
100
Cargo.lock
generated
100
Cargo.lock
generated
@@ -2,6 +2,15 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
@@ -828,6 +837,15 @@ version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
@@ -855,6 +873,15 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-bigint-dig"
|
||||
version = "0.8.6"
|
||||
@@ -1113,6 +1140,23 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
@@ -1212,6 +1256,8 @@ dependencies = [
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -1307,6 +1353,15 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -1646,6 +1701,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
@@ -1779,6 +1843,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1862,6 +1956,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
||||
@@ -13,4 +13,6 @@ serde_json = "1.0.149"
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "tls-rustls", "postgres", "uuid", "json", "chrono"] }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
toml = "1.0.7"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
uuid = { version = "1.22.0", features = ["serde", "v4"] }
|
||||
|
||||
32
README.md
32
README.md
@@ -11,11 +11,10 @@ cargo build --release
|
||||
# 或从 Release 页面下载预编译二进制
|
||||
```
|
||||
|
||||
配置数据库连接:
|
||||
配置数据库连接(首次使用需执行一次,之后在该设备上持久生效):
|
||||
|
||||
```bash
|
||||
export DATABASE_URL=postgres://postgres:<password>@<host>:5432/secrets
|
||||
# 或在项目根目录创建 .env 文件写入上述变量
|
||||
secrets config set-db "postgres://postgres:<password>@<host>:5432/secrets"
|
||||
```
|
||||
|
||||
## 使用
|
||||
@@ -30,6 +29,7 @@ secrets --help
|
||||
secrets -h
|
||||
|
||||
# 查看子命令帮助
|
||||
secrets help config
|
||||
secrets help add
|
||||
secrets help search
|
||||
secrets help delete
|
||||
@@ -54,6 +54,13 @@ secrets search --tag hongkong
|
||||
secrets search -q mqtt # 关键词匹配 name / metadata / tags
|
||||
secrets search -n refining --kind service --name gitea --show-secrets
|
||||
|
||||
# 开启 debug 级别日志(--verbose / -v,全局参数)
|
||||
secrets --verbose search -q mqtt
|
||||
secrets -v add -n refining --kind service --name gitea -m url=xxx -s token=yyy
|
||||
|
||||
# 或通过环境变量精细控制日志级别
|
||||
RUST_LOG=secrets=trace secrets search
|
||||
|
||||
# 增量更新已有记录(合并语义,记录不存在则报错)
|
||||
secrets update -n refining --kind server --name my-server -m ip=10.0.0.1
|
||||
secrets update -n refining --kind service --name gitea --add-tag production -s token=<new-token>
|
||||
@@ -65,7 +72,7 @@ secrets delete -n refining --kind server --name my-server
|
||||
|
||||
## 数据模型
|
||||
|
||||
单张 `secrets` 表,首次连接自动建表。
|
||||
单张 `secrets` 表,首次连接自动建表;同时自动创建 `audit_log` 表,记录所有写操作。
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
@@ -78,15 +85,30 @@ secrets delete -n refining --kind server --name my-server
|
||||
|
||||
`-m` / `--meta` 写入 `metadata`,`-s` / `--secret` 写入 `encrypted`,`value=@file` 从文件读取内容。
|
||||
|
||||
## 审计日志
|
||||
|
||||
`add`、`update`、`delete` 操作成功后自动向 `audit_log` 表写入一条记录,包含操作类型、操作对象和变更摘要(不含 secret 值)。操作者取自 `$USER` 环境变量。
|
||||
|
||||
```sql
|
||||
-- 查看最近 20 条审计记录
|
||||
SELECT action, namespace, kind, name, actor, detail, created_at
|
||||
FROM audit_log
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
src/
|
||||
main.rs # CLI 入口(clap)
|
||||
db.rs # 连接池 + auto-migrate
|
||||
config.rs # 配置读写(~/.config/secrets/config.toml)
|
||||
db.rs # 连接池 + auto-migrate(secrets + audit_log)
|
||||
models.rs # Secret 结构体
|
||||
audit.rs # 审计日志写入(audit_log 表)
|
||||
commands/
|
||||
add.rs # upsert
|
||||
config.rs # config set-db/show/path
|
||||
search.rs # 多条件查询
|
||||
delete.rs # 删除
|
||||
update.rs # 增量更新(合并 tags/metadata/encrypted)
|
||||
|
||||
34
src/audit.rs
Normal file
34
src/audit.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::Value;
|
||||
use sqlx::PgPool;
|
||||
|
||||
/// Write an audit entry for a write operation. Failures are logged as warnings
|
||||
/// and do not interrupt the main flow.
|
||||
pub async fn log(
|
||||
pool: &PgPool,
|
||||
action: &str,
|
||||
namespace: &str,
|
||||
kind: &str,
|
||||
name: &str,
|
||||
detail: Value,
|
||||
) {
|
||||
let actor = std::env::var("USER").unwrap_or_default();
|
||||
let result: Result<_, sqlx::Error> = sqlx::query(
|
||||
"INSERT INTO audit_log (action, namespace, kind, name, detail, actor) \
|
||||
VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
)
|
||||
.bind(action)
|
||||
.bind(namespace)
|
||||
.bind(kind)
|
||||
.bind(name)
|
||||
.bind(&detail)
|
||||
.bind(&actor)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
if let Err(e) = result {
|
||||
tracing::warn!(error = %e, "failed to write audit log");
|
||||
} else {
|
||||
tracing::debug!(action, namespace, kind, name, actor, "audit logged");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::{Map, Value};
|
||||
use serde_json::{Map, Value, json};
|
||||
use sqlx::PgPool;
|
||||
use std::fs;
|
||||
|
||||
@@ -43,6 +43,8 @@ pub async fn run(
|
||||
let metadata = build_json(meta_entries)?;
|
||||
let encrypted = build_json(secret_entries)?;
|
||||
|
||||
tracing::debug!(namespace, kind, name, "upserting record");
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO secrets (namespace, kind, name, tags, metadata, encrypted, updated_at)
|
||||
@@ -64,23 +66,38 @@ pub async fn run(
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
let meta_keys: Vec<&str> = meta_entries
|
||||
.iter()
|
||||
.filter_map(|s| s.split_once('=').map(|(k, _)| k))
|
||||
.collect();
|
||||
let secret_keys: Vec<&str> = secret_entries
|
||||
.iter()
|
||||
.filter_map(|s| s.split_once('=').map(|(k, _)| k))
|
||||
.collect();
|
||||
|
||||
crate::audit::log(
|
||||
pool,
|
||||
"add",
|
||||
namespace,
|
||||
kind,
|
||||
name,
|
||||
json!({
|
||||
"tags": tags,
|
||||
"meta_keys": meta_keys,
|
||||
"secret_keys": secret_keys,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
println!("Added: [{}/{}] {}", namespace, kind, name);
|
||||
if !tags.is_empty() {
|
||||
println!(" tags: {}", tags.join(", "));
|
||||
}
|
||||
if !meta_entries.is_empty() {
|
||||
let keys: Vec<&str> = meta_entries
|
||||
.iter()
|
||||
.filter_map(|s| s.split_once('=').map(|(k, _)| k))
|
||||
.collect();
|
||||
println!(" metadata: {}", keys.join(", "));
|
||||
println!(" metadata: {}", meta_keys.join(", "));
|
||||
}
|
||||
if !secret_entries.is_empty() {
|
||||
let keys: Vec<&str> = secret_entries
|
||||
.iter()
|
||||
.filter_map(|s| s.split_once('=').map(|(k, _)| k))
|
||||
.collect();
|
||||
println!(" secrets: {}", keys.join(", "));
|
||||
println!(" secrets: {}", secret_keys.join(", "));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::json;
|
||||
use sqlx::PgPool;
|
||||
|
||||
pub async fn run(pool: &PgPool, namespace: &str, kind: &str, name: &str) -> Result<()> {
|
||||
tracing::debug!(namespace, kind, name, "deleting record");
|
||||
|
||||
let result =
|
||||
sqlx::query("DELETE FROM secrets WHERE namespace = $1 AND kind = $2 AND name = $3")
|
||||
.bind(namespace)
|
||||
@@ -11,8 +14,10 @@ pub async fn run(pool: &PgPool, namespace: &str, kind: &str, name: &str) -> Resu
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
tracing::warn!(namespace, kind, name, "record not found for deletion");
|
||||
println!("Not found: [{}/{}] {}", namespace, kind, name);
|
||||
} else {
|
||||
crate::audit::log(pool, "delete", namespace, kind, name, json!({})).await;
|
||||
println!("Deleted: [{}/{}] {}", namespace, kind, name);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -44,6 +44,8 @@ pub async fn run(
|
||||
where_clause
|
||||
);
|
||||
|
||||
tracing::debug!(sql, "executing search query");
|
||||
|
||||
let mut q = sqlx::query_as::<_, Secret>(&sql);
|
||||
if let Some(v) = namespace {
|
||||
q = q.bind(v);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use serde_json::{Map, Value};
|
||||
use serde_json::{Map, Value, json};
|
||||
use sqlx::{FromRow, PgPool};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -85,6 +85,13 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>) -> Result<()> {
|
||||
}
|
||||
let encrypted = Value::Object(enc_map);
|
||||
|
||||
tracing::debug!(
|
||||
namespace = args.namespace,
|
||||
kind = args.kind,
|
||||
name = args.name,
|
||||
"updating record"
|
||||
);
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE secrets
|
||||
@@ -99,6 +106,34 @@ pub async fn run(pool: &PgPool, args: UpdateArgs<'_>) -> Result<()> {
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
let meta_keys: Vec<&str> = args
|
||||
.meta_entries
|
||||
.iter()
|
||||
.filter_map(|s| s.split_once('=').map(|(k, _)| k))
|
||||
.collect();
|
||||
let secret_keys: Vec<&str> = args
|
||||
.secret_entries
|
||||
.iter()
|
||||
.filter_map(|s| s.split_once('=').map(|(k, _)| k))
|
||||
.collect();
|
||||
|
||||
crate::audit::log(
|
||||
pool,
|
||||
"update",
|
||||
args.namespace,
|
||||
args.kind,
|
||||
args.name,
|
||||
json!({
|
||||
"add_tags": args.add_tags,
|
||||
"remove_tags": args.remove_tags,
|
||||
"meta_keys": meta_keys,
|
||||
"remove_meta": args.remove_meta,
|
||||
"secret_keys": secret_keys,
|
||||
"remove_secrets": args.remove_secrets,
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
println!("Updated: [{}/{}] {}", args.namespace, args.kind, args.name);
|
||||
|
||||
if !args.add_tags.is_empty() {
|
||||
|
||||
18
src/db.rs
18
src/db.rs
@@ -3,14 +3,17 @@ use sqlx::PgPool;
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
|
||||
pub async fn create_pool(database_url: &str) -> Result<PgPool> {
|
||||
tracing::debug!("connecting to database");
|
||||
let pool = PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect(database_url)
|
||||
.await?;
|
||||
tracing::debug!("database connection established");
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn migrate(pool: &PgPool) -> Result<()> {
|
||||
tracing::debug!("running migrations");
|
||||
sqlx::raw_sql(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS secrets (
|
||||
@@ -36,9 +39,24 @@ pub async fn migrate(pool: &PgPool) -> Result<()> {
|
||||
CREATE INDEX IF NOT EXISTS idx_secrets_kind ON secrets(kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_secrets_tags ON secrets USING GIN(tags);
|
||||
CREATE INDEX IF NOT EXISTS idx_secrets_metadata ON secrets USING GIN(metadata jsonb_path_ops);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
action VARCHAR(32) NOT NULL,
|
||||
namespace VARCHAR(64) NOT NULL,
|
||||
kind VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(256) NOT NULL,
|
||||
detail JSONB NOT NULL DEFAULT '{}',
|
||||
actor VARCHAR(128) NOT NULL DEFAULT '',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_audit_log_ns_kind ON audit_log(namespace, kind);
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
tracing::debug!("migrations complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
23
src/main.rs
23
src/main.rs
@@ -1,3 +1,4 @@
|
||||
mod audit;
|
||||
mod commands;
|
||||
mod config;
|
||||
mod db;
|
||||
@@ -5,6 +6,7 @@ mod models;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
@@ -17,6 +19,10 @@ struct Cli {
|
||||
#[arg(long, global = true, default_value = "")]
|
||||
db_url: String,
|
||||
|
||||
/// Enable verbose debug output
|
||||
#[arg(long, short, global = true)]
|
||||
verbose: bool,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
@@ -132,6 +138,16 @@ enum ConfigAction {
|
||||
async fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let filter = if cli.verbose {
|
||||
EnvFilter::new("secrets=debug")
|
||||
} else {
|
||||
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("secrets=warn"))
|
||||
};
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(filter)
|
||||
.with_target(false)
|
||||
.init();
|
||||
|
||||
// config 子命令不需要数据库连接,提前处理
|
||||
if let Commands::Config { action } = &cli.command {
|
||||
let cmd_action = match action {
|
||||
@@ -157,6 +173,8 @@ async fn main() -> Result<()> {
|
||||
meta,
|
||||
secrets,
|
||||
} => {
|
||||
let _span =
|
||||
tracing::info_span!("cmd", command = "add", %namespace, %kind, %name).entered();
|
||||
commands::add::run(&pool, namespace, kind, name, tags, meta, secrets).await?;
|
||||
}
|
||||
Commands::Search {
|
||||
@@ -166,6 +184,7 @@ async fn main() -> Result<()> {
|
||||
query,
|
||||
show_secrets,
|
||||
} => {
|
||||
let _span = tracing::info_span!("cmd", command = "search").entered();
|
||||
commands::search::run(
|
||||
&pool,
|
||||
namespace.as_deref(),
|
||||
@@ -181,6 +200,8 @@ async fn main() -> Result<()> {
|
||||
kind,
|
||||
name,
|
||||
} => {
|
||||
let _span =
|
||||
tracing::info_span!("cmd", command = "delete", %namespace, %kind, %name).entered();
|
||||
commands::delete::run(&pool, namespace, kind, name).await?;
|
||||
}
|
||||
Commands::Update {
|
||||
@@ -194,6 +215,8 @@ async fn main() -> Result<()> {
|
||||
secrets,
|
||||
remove_secrets,
|
||||
} => {
|
||||
let _span =
|
||||
tracing::info_span!("cmd", command = "update", %namespace, %kind, %name).entered();
|
||||
commands::update::run(
|
||||
&pool,
|
||||
commands::update::UpdateArgs {
|
||||
|
||||
Reference in New Issue
Block a user