mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 03:10:38 +09:00
Implement secrets service with local encryption, redaction, and runtime resolution
Add AES-256-GCM local encrypted secrets provider with auto-generated master key, stub providers for AWS/GCP/Vault, and a secrets service that normalizes adapter configs (converting sensitive inline values to secret refs in strict mode) and resolves secret refs back to plain values at runtime. Extract redaction utilities from agent routes into shared module. Redact sensitive values in activity logs, config revisions, and approval payloads. Block rollback of revisions containing redacted secrets. Filter hidden issues from list queries. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d26b67ebc3
commit
11901ae5d8
22 changed files with 1083 additions and 61 deletions
165
server/src/routes/secrets.ts
Normal file
165
server/src/routes/secrets.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import { Router } from "express";
|
||||
import type { Db } from "@paperclip/db";
|
||||
import {
|
||||
SECRET_PROVIDERS,
|
||||
type SecretProvider,
|
||||
createSecretSchema,
|
||||
rotateSecretSchema,
|
||||
updateSecretSchema,
|
||||
} from "@paperclip/shared";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { assertBoard, assertCompanyAccess } from "./authz.js";
|
||||
import { logActivity, secretService } from "../services/index.js";
|
||||
|
||||
export function secretRoutes(db: Db) {
|
||||
const router = Router();
|
||||
const svc = secretService(db);
|
||||
const configuredDefaultProvider = process.env.PAPERCLIP_SECRETS_PROVIDER;
|
||||
const defaultProvider = (
|
||||
configuredDefaultProvider && SECRET_PROVIDERS.includes(configuredDefaultProvider as SecretProvider)
|
||||
? configuredDefaultProvider
|
||||
: "local_encrypted"
|
||||
) as SecretProvider;
|
||||
|
||||
router.get("/companies/:companyId/secret-providers", (req, res) => {
|
||||
assertBoard(req);
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
res.json(svc.listProviders());
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/secrets", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const secrets = await svc.list(companyId);
|
||||
res.json(secrets);
|
||||
});
|
||||
|
||||
router.post("/companies/:companyId/secrets", validate(createSecretSchema), async (req, res) => {
|
||||
assertBoard(req);
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
|
||||
const created = await svc.create(
|
||||
companyId,
|
||||
{
|
||||
name: req.body.name,
|
||||
provider: req.body.provider ?? defaultProvider,
|
||||
value: req.body.value,
|
||||
description: req.body.description,
|
||||
externalRef: req.body.externalRef,
|
||||
},
|
||||
{ userId: req.actor.userId ?? "board", agentId: null },
|
||||
);
|
||||
|
||||
await logActivity(db, {
|
||||
companyId,
|
||||
actorType: "user",
|
||||
actorId: req.actor.userId ?? "board",
|
||||
action: "secret.created",
|
||||
entityType: "secret",
|
||||
entityId: created.id,
|
||||
details: { name: created.name, provider: created.provider },
|
||||
});
|
||||
|
||||
res.status(201).json(created);
|
||||
});
|
||||
|
||||
router.post("/secrets/:id/rotate", validate(rotateSecretSchema), async (req, res) => {
|
||||
assertBoard(req);
|
||||
const id = req.params.id as string;
|
||||
const existing = await svc.getById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Secret not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
|
||||
const rotated = await svc.rotate(
|
||||
id,
|
||||
{
|
||||
value: req.body.value,
|
||||
externalRef: req.body.externalRef,
|
||||
},
|
||||
{ userId: req.actor.userId ?? "board", agentId: null },
|
||||
);
|
||||
|
||||
await logActivity(db, {
|
||||
companyId: rotated.companyId,
|
||||
actorType: "user",
|
||||
actorId: req.actor.userId ?? "board",
|
||||
action: "secret.rotated",
|
||||
entityType: "secret",
|
||||
entityId: rotated.id,
|
||||
details: { version: rotated.latestVersion },
|
||||
});
|
||||
|
||||
res.json(rotated);
|
||||
});
|
||||
|
||||
router.patch("/secrets/:id", validate(updateSecretSchema), async (req, res) => {
|
||||
assertBoard(req);
|
||||
const id = req.params.id as string;
|
||||
const existing = await svc.getById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Secret not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
|
||||
const updated = await svc.update(id, {
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
externalRef: req.body.externalRef,
|
||||
});
|
||||
|
||||
if (!updated) {
|
||||
res.status(404).json({ error: "Secret not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
await logActivity(db, {
|
||||
companyId: updated.companyId,
|
||||
actorType: "user",
|
||||
actorId: req.actor.userId ?? "board",
|
||||
action: "secret.updated",
|
||||
entityType: "secret",
|
||||
entityId: updated.id,
|
||||
details: { name: updated.name },
|
||||
});
|
||||
|
||||
res.json(updated);
|
||||
});
|
||||
|
||||
router.delete("/secrets/:id", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const id = req.params.id as string;
|
||||
const existing = await svc.getById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Secret not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
|
||||
const removed = await svc.remove(id);
|
||||
if (!removed) {
|
||||
res.status(404).json({ error: "Secret not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
await logActivity(db, {
|
||||
companyId: removed.companyId,
|
||||
actorType: "user",
|
||||
actorId: req.actor.userId ?? "board",
|
||||
action: "secret.deleted",
|
||||
entityType: "secret",
|
||||
entityId: removed.id,
|
||||
details: { name: removed.name },
|
||||
});
|
||||
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue