fix vault 403 + shared secrets (v1.21.0)
- 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>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
'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 };
|
||||
Reference in New Issue
Block a user