mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-17 21:25:49 +02:00
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/
137 lines
3.4 KiB
Go
137 lines
3.4 KiB
Go
package cli
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/j3ssie/osmedeus/v5/internal/config"
|
|
"github.com/j3ssie/osmedeus/v5/internal/database"
|
|
)
|
|
|
|
// activeOrgFileName is the file under the base folder holding the org selected by
|
|
// `osmedeus org use`.
|
|
const activeOrgFileName = ".active-org"
|
|
|
|
var (
|
|
resolvedOrgUUID string
|
|
resolveOrgOnce sync.Once
|
|
resolveOrgErr error
|
|
)
|
|
|
|
// activeOrgFilePath returns the path of the persisted active-org file.
|
|
func activeOrgFilePath() string {
|
|
base := ""
|
|
if cfg := config.Get(); cfg != nil {
|
|
base = cfg.BaseFolder
|
|
}
|
|
if base == "" {
|
|
base = baseFolder
|
|
}
|
|
if base == "" {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
base = filepath.Join(home, "osmedeus-base")
|
|
}
|
|
return filepath.Join(base, activeOrgFileName)
|
|
}
|
|
|
|
// readActiveOrg returns the persisted active org reference, or "" if none is set.
|
|
func readActiveOrg() string {
|
|
path := activeOrgFilePath()
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(data))
|
|
}
|
|
|
|
// writeActiveOrg persists the active org reference.
|
|
func writeActiveOrg(orgUUID string) error {
|
|
path := activeOrgFilePath()
|
|
if path == "" {
|
|
return fmt.Errorf("cannot determine base folder for active org file")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(path, []byte(orgUUID+"\n"), 0o644)
|
|
}
|
|
|
|
// clearActiveOrg removes the persisted active org, returning to the unfiltered
|
|
// "all orgs" view.
|
|
func clearActiveOrg() error {
|
|
path := activeOrgFilePath()
|
|
if path == "" {
|
|
return nil
|
|
}
|
|
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// orgRef returns the raw org reference (name or UUID) from the highest-priority
|
|
// source that has one, without touching the database. Resolution order:
|
|
//
|
|
// 1. --org flag
|
|
// 2. $OSMEDEUS_ORG_UUID
|
|
// 3. $OSMEDEUS_ORG
|
|
// 4. the active-org file written by `osmedeus org use`
|
|
// 5. "" — meaning no org was selected
|
|
func orgRef() string {
|
|
if v := strings.TrimSpace(globalOrg); v != "" {
|
|
return v
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv("OSMEDEUS_ORG_UUID")); v != "" {
|
|
return v
|
|
}
|
|
if v := strings.TrimSpace(os.Getenv("OSMEDEUS_ORG")); v != "" {
|
|
return v
|
|
}
|
|
return readActiveOrg()
|
|
}
|
|
|
|
// resolveOrgUUID resolves the selected org to a UUID, once per process.
|
|
//
|
|
// The memo matters: a multi-target run resolves the org once per target, and all
|
|
// targets share one constant reference.
|
|
//
|
|
// An empty return means no org was selected. Read paths must treat that as "no
|
|
// filter" so a database with no orgs behaves exactly as it did before the org
|
|
// layer existed; write paths must run it through database.NormalizeOrgUUID so
|
|
// rows land in the default org rather than being stranded under an empty string.
|
|
//
|
|
// Requires a connected database; callers that may run without one should check
|
|
// database.GetDB() first.
|
|
func resolveOrgUUID(ctx context.Context) (string, error) {
|
|
resolveOrgOnce.Do(func() {
|
|
ref := orgRef()
|
|
if ref == "" {
|
|
return
|
|
}
|
|
orgUUID, err := database.ResolveOrgUUID(ctx, ref)
|
|
if err != nil {
|
|
resolveOrgErr = fmt.Errorf("failed to resolve --org %q: %w", ref, err)
|
|
return
|
|
}
|
|
resolvedOrgUUID = orgUUID
|
|
})
|
|
return resolvedOrgUUID, resolveOrgErr
|
|
}
|
|
|
|
// resetOrgResolution clears the cached resolution. Tests only.
|
|
func resetOrgResolution() {
|
|
resolveOrgOnce = sync.Once{}
|
|
resolvedOrgUUID = ""
|
|
resolveOrgErr = nil
|
|
}
|