Files
j3ssie 77bad65cd9 feat: v5.1.0 — orgs, npm distribution, bundled skills, platform vendoring
Org (tenant) layer
- New Org model with org_uuid denormalized onto workspaces, assets,
  vulnerabilities and runs so cross-workspace queries need no join
- Automatic attribution via BeforeAppendModel hooks; importers stay org-unaware
- Read semantics: empty org means no filter (backward compatible)
  Write semantics: empty org coerced to the default org
- Migration backfills every pre-existing row into the default org
- CLI: osmedeus org create/show/assign/use/rename/delete
- API: /osm/api/orgs CRUD plus ?org= on assets, vulns, runs and workspaces

npm distribution
- npm install -g @j3ssie/osmedeus ships the Go binary through npm
- One npm name with version-suffixed platform builds pulled in as aliased
  optionalDependencies, so an install downloads exactly one binary
- Binary ships gzipped and is decompressed on first run into a
  version-scoped cache, so an upgrade can never exec a stale binary
- make bump-version is the single source of truth for the version constant

Bundled agent skills
- public/skills/ embedded in the binary, installed via osmedeus skills install
- Filesystem-driven discovery: a new bundle needs no code change
- make sync-skills mirrors bundles out to the standalone skills repo

Platform sub-projects
- Vendor dashboard, registry and workflow under platform/ so they version
  with the engine they talk to; make sync-platform publishes them out
- Rebuild the embedded UI in public/ui/
2026-08-08 22:26:09 +08:00

121 lines
3.9 KiB
Go

package database
import (
"context"
"sync"
"github.com/uptrace/bun"
)
// The BeforeAppendModel hooks below are the last line of defence for org
// attribution. Bun persists the Go zero value rather than falling back to the
// column DEFAULT, so any write that forgets to set OrgUUID would store an empty
// string - a value that matches no org filter and hides the row from every query.
//
// The hooks fire on INSERT and UPDATE alike, because the import paths build a
// fresh struct from scan output and UPDATE it WherePK; such a struct has an empty
// OrgUUID meaning "not loaded", not "default org". Resolving from the row's
// workspace produces the right answer in both cases, since org membership is
// assigned per workspace and cascaded (see AssignWorkspacesToOrg).
//
// Writes that deliberately set org_uuid — `org assign`, `org delete` — use raw
// UPDATE statements without a model, so these hooks never fire on them.
//
// A non-empty OrgUUID is always left untouched.
var _ bun.BeforeAppendModelHook = (*Asset)(nil)
var _ bun.BeforeAppendModelHook = (*Vulnerability)(nil)
var _ bun.BeforeAppendModelHook = (*Run)(nil)
var _ bun.BeforeAppendModelHook = (*Workspace)(nil)
// orgForWrite returns the org a model should be persisted under: the one it
// already names, or the org of the workspace it belongs to.
//
// Passing an empty workspace yields the default org, which is what Workspace
// itself wants — it is the root of the mapping, with nothing to derive from.
func orgForWrite(ctx context.Context, query bun.Query, current, workspace string) string {
if current != "" {
return current
}
switch query.(type) {
case *bun.InsertQuery, *bun.UpdateQuery:
return GetOrgUUIDForWorkspace(ctx, workspace)
}
return current
}
func (a *Asset) BeforeAppendModel(ctx context.Context, query bun.Query) error {
a.OrgUUID = orgForWrite(ctx, query, a.OrgUUID, a.Workspace)
return nil
}
func (v *Vulnerability) BeforeAppendModel(ctx context.Context, query bun.Query) error {
v.OrgUUID = orgForWrite(ctx, query, v.OrgUUID, v.Workspace)
return nil
}
func (r *Run) BeforeAppendModel(ctx context.Context, query bun.Query) error {
r.OrgUUID = orgForWrite(ctx, query, r.OrgUUID, r.Workspace)
return nil
}
func (w *Workspace) BeforeAppendModel(ctx context.Context, query bun.Query) error {
w.OrgUUID = orgForWrite(ctx, query, w.OrgUUID, "")
return nil
}
// workspaceOrgCache memoizes workspace name -> org UUID so per-line importers do
// not issue a lookup per row. Invalidated wholesale whenever org membership
// changes, which is rare.
var (
workspaceOrgCache = make(map[string]string)
workspaceOrgCacheMu sync.RWMutex
)
// invalidateWorkspaceOrgCache drops every memoized workspace -> org mapping.
func invalidateWorkspaceOrgCache() {
workspaceOrgCacheMu.Lock()
workspaceOrgCache = make(map[string]string)
workspaceOrgCacheMu.Unlock()
}
// GetOrgUUIDForWorkspace returns the org a workspace belongs to, falling back to
// the default org when the workspace has no row yet (the first import of a scan
// can land before EnsureWorkspaceRuntime).
//
// Deriving the org from the workspace rather than threading it through every
// import function means org membership always follows the workspace: reassigning
// a workspace with `osmedeus org assign` retargets its future imports too.
func GetOrgUUIDForWorkspace(ctx context.Context, workspace string) string {
if workspace == "" {
return DefaultOrgUUID
}
workspaceOrgCacheMu.RLock()
cached, ok := workspaceOrgCache[workspace]
workspaceOrgCacheMu.RUnlock()
if ok {
return cached
}
if db == nil {
return DefaultOrgUUID
}
var orgUUID string
err := db.NewSelect().Model((*Workspace)(nil)).
Column("org_uuid").
Where("name = ?", workspace).
Limit(1).
Scan(ctx, &orgUUID)
if err != nil || orgUUID == "" {
// Do not cache a miss: the workspace row usually appears moments later.
return DefaultOrgUUID
}
workspaceOrgCacheMu.Lock()
workspaceOrgCache[workspace] = orgUUID
workspaceOrgCacheMu.Unlock()
return orgUUID
}