948fef4adc
- vault_broker: always reconcile policy content before serving a cached token (compare-and-skip), so stale stored policies can't cause a recurring 403 'permission denied'; policy content is parsed live by OpenBao, so edits apply to existing tokens immediately. - Shared secrets: publish to secret/shared/<owner>/<slug>; grant read to users and apps by editing the grantee's policy content (live-applied). New SharedSecret/SharedSecretGrant ORM models, /api/shared-secrets router, and a Shared tab in the vault UI. - package.json + lockfile bumped to 1.21.0 to match the tag. Co-Authored-By: Claude <noreply@anthropic.com>
57 lines
2.4 KiB
JavaScript
57 lines
2.4 KiB
JavaScript
'use strict';
|
|
|
|
// SharedSecret — a secret the owner has published to the shared namespace so it
|
|
// can be shared with other users and/or downstream apps.
|
|
//
|
|
// The secret DATA lives in OpenBao at `secret/shared/<ownerUid>/<slug>` (KV-v2),
|
|
// never in the DB. This row is metadata only (owner + slug + description) and is
|
|
// the source of truth for the UI (which shares exist). ACCESS CONTROL is enforced
|
|
// entirely by OpenBao ACL policies: the owner's `user-<uid>` policy grants full
|
|
// R/W on `secret/shared/<ownerUid>/*`, and each grantee's policy content is
|
|
// edited to add `read` on the exact shared path (see vault_broker.js — policy
|
|
// content is parsed live at token use, so a grant takes effect immediately with
|
|
// no token re-mint). `secretId` on SharedSecretGrant links grantees to this row.
|
|
//
|
|
// `slug` is unique and immutable in practice — it is embedded in the shared path
|
|
// and in grantee policy rules, so changing it would require rewriting policies.
|
|
// Like PluginInstance, there is no ORM auto-timestamp hook: route handlers stamp
|
|
// created_by/on + updated_by/on on every write. `id` (uuid) is generated by the
|
|
// ORM on create.
|
|
|
|
const { Model } = require('@simpleworkjs/orm');
|
|
|
|
class SharedSecret extends Model {
|
|
static fields = {
|
|
id: { type: 'uuid', primaryKey: true },
|
|
// Human slug embedded in the OpenBao path: secret/shared/<ownerUid>/<slug>.
|
|
// Unique so two owners can't collide on the same shared path.
|
|
slug: { type: 'string', isRequired: true, unique: true, min: 1, max: 64 },
|
|
// The publishing user's uid — also the shared path's namespace segment.
|
|
ownerUid: { type: 'string', isRequired: true, min: 1, max: 64 },
|
|
// Optional human description shown in the Shared tab.
|
|
description: { type: 'text' },
|
|
// Audit stamps (set by the route handler, not by an ORM hook).
|
|
created_by: { type: 'string' },
|
|
created_on: { type: 'integer' },
|
|
updated_by: { type: 'string' },
|
|
updated_on: { type: 'integer' },
|
|
};
|
|
|
|
// Full OpenBao KV-v2 path for this shared secret (logical path, no data/metadata).
|
|
static pathFor(ownerUid, slug) {
|
|
return `shared/${ownerUid}/${slug}`;
|
|
}
|
|
|
|
path() {
|
|
return SharedSecret.pathFor(this.ownerUid, this.slug);
|
|
}
|
|
|
|
// Look up by slug (unique). Returns the row or null.
|
|
static async getBySlug(slug) {
|
|
const rows = await this.list({ where: { slug } });
|
|
return rows[0] || null;
|
|
}
|
|
}
|
|
|
|
module.exports = { SharedSecret };
|