feat(server): org-scope service layer + handlers + org admin API
Threads org_id through every admin-facing service function (servers, keys, assignments, secrets, workflows/steps/runs, monitors, channels, audit), adds RequireRole middleware, and wires /api/org user + OIDC management routes. Agent/scheduler paths keep unique-key signatures and resolve org from the loaded record; internal-only helpers (getServerByID, getRunByID, getMonitorByID) preserve those call sites.
This commit is contained in:
@@ -11,11 +11,12 @@ import (
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func LogEvent(eventType, actor, serverID, keyID, details string) {
|
||||
func LogEvent(orgID, eventType, actor, serverID, keyID, details string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
event := models.AuditEvent{
|
||||
OrgID: orgID,
|
||||
EventType: eventType,
|
||||
Actor: actor,
|
||||
ServerID: serverID,
|
||||
@@ -28,7 +29,7 @@ func LogEvent(eventType, actor, serverID, keyID, details string) {
|
||||
}
|
||||
}
|
||||
|
||||
func ListAuditEvents(limit int64) ([]models.AuditEvent, error) {
|
||||
func ListAuditEvents(orgID string, limit int64) ([]models.AuditEvent, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -36,7 +37,7 @@ func ListAuditEvents(limit int64) ([]models.AuditEvent, error) {
|
||||
SetSort(bson.D{{Key: "created_at", Value: -1}}).
|
||||
SetLimit(limit)
|
||||
|
||||
cursor, err := db.Col("audit_logs").Find(ctx, bson.M{}, opts)
|
||||
cursor, err := db.Col("audit_logs").Find(ctx, bson.M{"org_id": orgID}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ import (
|
||||
"go.mongodb.org/mongo-driver/v2/mongo/options"
|
||||
)
|
||||
|
||||
func ListChannels() ([]models.NotificationChannel, error) {
|
||||
func ListChannels(orgID string) ([]models.NotificationChannel, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
cur, err := db.Col("notification_channels").Find(ctx, bson.M{}, options.Find().SetSort(bson.M{"created_at": 1}))
|
||||
cur, err := db.Col("notification_channels").Find(ctx, bson.M{"org_id": orgID}, options.Find().SetSort(bson.M{"created_at": 1}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -27,11 +27,11 @@ func ListChannels() ([]models.NotificationChannel, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func GetChannel(channelID string) (*models.NotificationChannel, error) {
|
||||
func GetChannel(orgID, channelID string) (*models.NotificationChannel, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
var ch models.NotificationChannel
|
||||
err := db.Col("notification_channels").FindOne(ctx, bson.M{"channel_id": channelID}).Decode(&ch)
|
||||
err := db.Col("notification_channels").FindOne(ctx, bson.M{"channel_id": channelID, "org_id": orgID}).Decode(&ch)
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -59,9 +59,10 @@ func GetChannels(channelIDs []string) ([]models.NotificationChannel, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func CreateChannel(ch *models.NotificationChannel) (*models.NotificationChannel, error) {
|
||||
func CreateChannel(orgID string, ch *models.NotificationChannel) (*models.NotificationChannel, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
ch.OrgID = orgID
|
||||
ch.ChannelID = uuid.NewString()
|
||||
ch.CreatedAt = time.Now()
|
||||
if ch.Config == nil {
|
||||
@@ -73,23 +74,23 @@ func CreateChannel(ch *models.NotificationChannel) (*models.NotificationChannel,
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func UpdateChannel(channelID string, upd bson.M) error {
|
||||
func UpdateChannel(orgID, channelID string, upd bson.M) error {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
_, err := db.Col("notification_channels").UpdateOne(ctx, bson.M{"channel_id": channelID}, bson.M{"$set": upd})
|
||||
_, err := db.Col("notification_channels").UpdateOne(ctx, bson.M{"channel_id": channelID, "org_id": orgID}, bson.M{"$set": upd})
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteChannel(channelID string) error {
|
||||
func DeleteChannel(orgID, channelID string) error {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
_, err := db.Col("notification_channels").DeleteOne(ctx, bson.M{"channel_id": channelID})
|
||||
_, err := db.Col("notification_channels").DeleteOne(ctx, bson.M{"channel_id": channelID, "org_id": orgID})
|
||||
return err
|
||||
}
|
||||
|
||||
// TestChannel sends a synthetic alert to verify configuration.
|
||||
func TestChannel(channelID string) error {
|
||||
ch, err := GetChannel(channelID)
|
||||
func TestChannel(orgID, channelID string) error {
|
||||
ch, err := GetChannel(orgID, channelID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func readDefaultStepFiles() ([]models.WorkflowStep, error) {
|
||||
|
||||
// SeedDefaultSteps upserts default steps from disk keyed on {slug, source}.
|
||||
// Re-sync overwrites default-step content; user steps are never touched.
|
||||
func SeedDefaultSteps() (created, updated int, err error) {
|
||||
func SeedDefaultSteps(orgID string) (created, updated int, err error) {
|
||||
steps, err := readDefaultStepFiles()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
@@ -61,7 +61,7 @@ func SeedDefaultSteps() (created, updated int, err error) {
|
||||
defer cancel()
|
||||
col := db.Col("workflow_steps")
|
||||
for _, s := range steps {
|
||||
filter := bson.M{"slug": s.Slug, "source": "default"}
|
||||
filter := bson.M{"org_id": orgID, "slug": s.Slug, "source": "default"}
|
||||
set := bson.M{
|
||||
"name": s.Name,
|
||||
"description": s.Description,
|
||||
@@ -75,6 +75,7 @@ func SeedDefaultSteps() (created, updated int, err error) {
|
||||
res, uerr := col.UpdateOne(ctx, filter, bson.M{
|
||||
"$set": set,
|
||||
"$setOnInsert": bson.M{
|
||||
"org_id": orgID,
|
||||
"step_id": uuid.New().String(),
|
||||
"slug": s.Slug,
|
||||
"source": "default",
|
||||
|
||||
@@ -36,8 +36,9 @@ func setKeyMeta(k *models.Key) {
|
||||
k.HasPassphrase = k.PassphraseEncrypted != ""
|
||||
}
|
||||
|
||||
func CreateKey(label, publicKey, source, generatedByServerID, privateKey, passphrase string) (*models.Key, error) {
|
||||
func CreateKey(orgID, label, publicKey, source, generatedByServerID, privateKey, passphrase string) (*models.Key, error) {
|
||||
key := &models.Key{
|
||||
OrgID: orgID,
|
||||
KeyID: uuid.NewString(),
|
||||
Label: label,
|
||||
PublicKey: publicKey,
|
||||
@@ -71,12 +72,12 @@ func CreateKey(label, publicKey, source, generatedByServerID, privateKey, passph
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func GetKey(keyID string) (*models.Key, error) {
|
||||
func GetKey(orgID, keyID string) (*models.Key, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var key models.Key
|
||||
err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID}).Decode(&key)
|
||||
err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID, "org_id": orgID}).Decode(&key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -84,12 +85,12 @@ func GetKey(keyID string) (*models.Key, error) {
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func GetPrivateKey(keyID string) (string, error) {
|
||||
func GetPrivateKey(orgID, keyID string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var key models.Key
|
||||
if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID}).Decode(&key); err != nil {
|
||||
if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID, "org_id": orgID}).Decode(&key); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if key.PrivateKeyEncrypted == "" {
|
||||
@@ -99,7 +100,8 @@ func GetPrivateKey(keyID string) (string, error) {
|
||||
}
|
||||
|
||||
// GetPassphrase returns the decrypted passphrase for a key, or an empty string
|
||||
// if the key has none stored.
|
||||
// if the key has none stored. Agent-path (keyed by unique key_id from an
|
||||
// assignment lookup) — no org filter.
|
||||
func GetPassphrase(keyID string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -119,11 +121,11 @@ type KeyWithCount struct {
|
||||
AssignedCount int `bson:"-" json:"assigned_count"`
|
||||
}
|
||||
|
||||
func ListKeys() ([]KeyWithCount, error) {
|
||||
func ListKeys(orgID string) ([]KeyWithCount, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cursor, err := db.Col("keys").Find(ctx, bson.M{})
|
||||
cursor, err := db.Col("keys").Find(ctx, bson.M{"org_id": orgID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -138,6 +140,7 @@ func ListKeys() ([]KeyWithCount, error) {
|
||||
for _, k := range keys {
|
||||
setKeyMeta(&k)
|
||||
count, _ := db.Col("assignments").CountDocuments(ctx, bson.M{
|
||||
"org_id": orgID,
|
||||
"key_id": k.KeyID,
|
||||
"revoked_at": nil,
|
||||
})
|
||||
@@ -146,19 +149,19 @@ func ListKeys() ([]KeyWithCount, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func DeleteKey(keyID string) error {
|
||||
func DeleteKey(orgID, keyID string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var key models.Key
|
||||
if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID}).Decode(&key); err != nil {
|
||||
if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID, "org_id": orgID}).Decode(&key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := db.Col("keys").DeleteOne(ctx, bson.M{"key_id": keyID}); err != nil {
|
||||
if _, err := db.Col("keys").DeleteOne(ctx, bson.M{"key_id": keyID, "org_id": orgID}); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.Col("assignments").DeleteMany(ctx, bson.M{"key_id": keyID}); err != nil {
|
||||
if _, err := db.Col("assignments").DeleteMany(ctx, bson.M{"key_id": keyID, "org_id": orgID}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -168,13 +171,14 @@ func DeleteKey(keyID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func AssignKey(keyID, serverID string) (*models.Assignment, error) {
|
||||
func AssignKey(orgID, keyID, serverID string) (*models.Assignment, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Check if already assigned and active
|
||||
var existing models.Assignment
|
||||
err := db.Col("assignments").FindOne(ctx, bson.M{
|
||||
"org_id": orgID,
|
||||
"key_id": keyID,
|
||||
"server_id": serverID,
|
||||
"revoked_at": nil,
|
||||
@@ -184,6 +188,7 @@ func AssignKey(keyID, serverID string) (*models.Assignment, error) {
|
||||
}
|
||||
|
||||
a := &models.Assignment{
|
||||
OrgID: orgID,
|
||||
KeyID: keyID,
|
||||
ServerID: serverID,
|
||||
AssignedAt: time.Now(),
|
||||
@@ -195,23 +200,23 @@ func AssignKey(keyID, serverID string) (*models.Assignment, error) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func RevokeAssignment(keyID, serverID string) error {
|
||||
func RevokeAssignment(orgID, keyID, serverID string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
now := time.Now()
|
||||
_, err := db.Col("assignments").UpdateOne(ctx,
|
||||
bson.M{"key_id": keyID, "server_id": serverID, "revoked_at": nil},
|
||||
bson.M{"org_id": orgID, "key_id": keyID, "server_id": serverID, "revoked_at": nil},
|
||||
bson.M{"$set": bson.M{"revoked_at": now}},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetAssignmentsForKey(keyID string) ([]models.Assignment, error) {
|
||||
func GetAssignmentsForKey(orgID, keyID string) ([]models.Assignment, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cursor, err := db.Col("assignments").Find(ctx, bson.M{"key_id": keyID, "revoked_at": nil})
|
||||
cursor, err := db.Col("assignments").Find(ctx, bson.M{"org_id": orgID, "key_id": keyID, "revoked_at": nil})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -229,11 +234,11 @@ type AssignmentWithServer struct {
|
||||
Server *models.Server `json:"server,omitempty"`
|
||||
}
|
||||
|
||||
func GetAssignmentsWithServers(keyID string) ([]AssignmentWithServer, error) {
|
||||
func GetAssignmentsWithServers(orgID, keyID string) ([]AssignmentWithServer, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cursor, err := db.Col("assignments").Find(ctx, bson.M{"key_id": keyID})
|
||||
cursor, err := db.Col("assignments").Find(ctx, bson.M{"org_id": orgID, "key_id": keyID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -248,7 +253,7 @@ func GetAssignmentsWithServers(keyID string) ([]AssignmentWithServer, error) {
|
||||
for _, a := range assignments {
|
||||
item := AssignmentWithServer{Assignment: a}
|
||||
var srv models.Server
|
||||
if err := db.Col("servers").FindOne(ctx, bson.M{"server_id": a.ServerID}).Decode(&srv); err == nil {
|
||||
if err := db.Col("servers").FindOne(ctx, bson.M{"server_id": a.ServerID, "org_id": orgID}).Decode(&srv); err == nil {
|
||||
item.Server = &srv
|
||||
}
|
||||
result = append(result, item)
|
||||
@@ -261,11 +266,11 @@ type AssignmentWithKey struct {
|
||||
Key *models.Key `json:"key,omitempty"`
|
||||
}
|
||||
|
||||
func GetAssignmentsWithKeysForServer(serverID string) ([]AssignmentWithKey, error) {
|
||||
func GetAssignmentsWithKeysForServer(orgID, serverID string) ([]AssignmentWithKey, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cursor, err := db.Col("assignments").Find(ctx, bson.M{"server_id": serverID})
|
||||
cursor, err := db.Col("assignments").Find(ctx, bson.M{"org_id": orgID, "server_id": serverID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -279,7 +284,7 @@ func GetAssignmentsWithKeysForServer(serverID string) ([]AssignmentWithKey, erro
|
||||
result := make([]AssignmentWithKey, 0, len(assignments))
|
||||
for _, a := range assignments {
|
||||
var key models.Key
|
||||
if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": a.KeyID}).Decode(&key); err != nil {
|
||||
if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": a.KeyID, "org_id": orgID}).Decode(&key); err != nil {
|
||||
continue
|
||||
}
|
||||
setKeyMeta(&key)
|
||||
|
||||
@@ -36,10 +36,10 @@ func SpecFor(m *models.Monitor) checker.Spec {
|
||||
}
|
||||
}
|
||||
|
||||
func ListMonitors() ([]models.Monitor, error) {
|
||||
func ListMonitors(orgID string) ([]models.Monitor, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
cur, err := db.Col("monitors").Find(ctx, bson.M{}, options.Find().SetSort(bson.M{"created_at": 1}))
|
||||
cur, err := db.Col("monitors").Find(ctx, bson.M{"org_id": orgID}, options.Find().SetSort(bson.M{"created_at": 1}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -51,6 +51,8 @@ func ListMonitors() ([]models.Monitor, error) {
|
||||
}
|
||||
|
||||
// ListMonitorsForRunner returns enabled monitors whose Runner matches runner.
|
||||
// Agent/scheduler path — a cross-org sweep (mirrors MarkOfflineServers), so it
|
||||
// intentionally has no org filter.
|
||||
func ListMonitorsForRunner(runner string) ([]models.Monitor, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
@@ -65,7 +67,24 @@ func ListMonitorsForRunner(runner string) ([]models.Monitor, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func GetMonitor(monitorID string) (*models.Monitor, error) {
|
||||
// GetMonitor looks up a monitor scoped to an org (handler/session use).
|
||||
func GetMonitor(orgID, monitorID string) (*models.Monitor, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
var m models.Monitor
|
||||
err := db.Col("monitors").FindOne(ctx, bson.M{"monitor_id": monitorID, "org_id": orgID}).Decode(&m)
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
// getMonitorByID looks up a monitor by its unique monitor_id with no org
|
||||
// filter. For agent/scheduler use only (IngestResult), which has no session.
|
||||
func getMonitorByID(monitorID string) (*models.Monitor, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
var m models.Monitor
|
||||
@@ -79,9 +98,10 @@ func GetMonitor(monitorID string) (*models.Monitor, error) {
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
func CreateMonitor(m *models.Monitor) (*models.Monitor, error) {
|
||||
func CreateMonitor(orgID string, m *models.Monitor) (*models.Monitor, error) {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
m.OrgID = orgID
|
||||
m.MonitorID = uuid.NewString()
|
||||
m.CreatedAt = time.Now()
|
||||
if m.IntervalSec <= 0 {
|
||||
@@ -100,17 +120,17 @@ func CreateMonitor(m *models.Monitor) (*models.Monitor, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func UpdateMonitor(monitorID string, upd bson.M) error {
|
||||
func UpdateMonitor(orgID, monitorID string, upd bson.M) error {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
_, err := db.Col("monitors").UpdateOne(ctx, bson.M{"monitor_id": monitorID}, bson.M{"$set": upd})
|
||||
_, err := db.Col("monitors").UpdateOne(ctx, bson.M{"monitor_id": monitorID, "org_id": orgID}, bson.M{"$set": upd})
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteMonitor(monitorID string) error {
|
||||
func DeleteMonitor(orgID, monitorID string) error {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
if _, err := db.Col("monitors").DeleteOne(ctx, bson.M{"monitor_id": monitorID}); err != nil {
|
||||
if _, err := db.Col("monitors").DeleteOne(ctx, bson.M{"monitor_id": monitorID, "org_id": orgID}); err != nil {
|
||||
return err
|
||||
}
|
||||
db.Col("incidents").DeleteMany(ctx, bson.M{"monitor_id": monitorID})
|
||||
@@ -161,7 +181,7 @@ func IngestResult(monitorID string, res checker.Result) error {
|
||||
ctx, cancel := monCtx()
|
||||
defer cancel()
|
||||
|
||||
m, err := GetMonitor(monitorID)
|
||||
m, err := getMonitorByID(monitorID)
|
||||
if err != nil || m == nil {
|
||||
return err
|
||||
}
|
||||
@@ -266,5 +286,5 @@ func notifyTransition(m *models.Monitor, newStatus, message string) {
|
||||
}
|
||||
}(ch)
|
||||
}
|
||||
_ = UpdateMonitor(m.MonitorID, bson.M{"state.last_notified_at": time.Now()})
|
||||
_ = UpdateMonitor(m.OrgID, m.MonitorID, bson.M{"state.last_notified_at": time.Now()})
|
||||
}
|
||||
|
||||
@@ -39,6 +39,27 @@ func GetOrgBySlug(slug string) (*models.Org, error) {
|
||||
return &o, nil
|
||||
}
|
||||
|
||||
// ListOrgIDs returns the org_id of every organization. Used by startup tasks
|
||||
// (e.g. seeding default workflow steps) that must run once per org.
|
||||
func ListOrgIDs() ([]string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
cursor, err := db.Col("orgs").Find(ctx, bson.M{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
var orgs []models.Org
|
||||
if err := cursor.All(ctx, &orgs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids := make([]string, 0, len(orgs))
|
||||
for _, o := range orgs {
|
||||
ids = append(ids, o.OrgID)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func CreateOrg(name string) (*models.Org, error) {
|
||||
base := Slugify(name)
|
||||
if len(base) < 3 {
|
||||
|
||||
@@ -27,11 +27,12 @@ func EnsureSecretIndexes() error {
|
||||
|
||||
// ListSecretGroups returns a summary of every group with its key count and
|
||||
// most recent update time.
|
||||
func ListSecretGroups() ([]models.GroupSummary, error) {
|
||||
func ListSecretGroups(orgID string) ([]models.GroupSummary, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pipeline := mongo.Pipeline{
|
||||
{{Key: "$match", Value: bson.D{{Key: "org_id", Value: orgID}}}},
|
||||
{{Key: "$group", Value: bson.D{
|
||||
{Key: "_id", Value: "$group"},
|
||||
{Key: "key_count", Value: bson.D{{Key: "$sum", Value: 1}}},
|
||||
@@ -68,11 +69,11 @@ func ListSecretGroups() ([]models.GroupSummary, error) {
|
||||
|
||||
// GetSecretGroup returns the keys within a group, sorted by key name, without
|
||||
// decrypted values.
|
||||
func GetSecretGroup(group string) ([]models.Secret, error) {
|
||||
func GetSecretGroup(orgID, group string) ([]models.Secret, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cursor, err := db.Col("secrets").Find(ctx, bson.M{"group": group},
|
||||
cursor, err := db.Col("secrets").Find(ctx, bson.M{"org_id": orgID, "group": group},
|
||||
options.Find().SetSort(bson.D{{Key: "key", Value: 1}}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -87,9 +88,11 @@ func GetSecretGroup(group string) ([]models.Secret, error) {
|
||||
}
|
||||
|
||||
// GetSecretGroupDecrypted returns a flat map of key → plaintext value for a
|
||||
// group. Used by the ESO read endpoint.
|
||||
func GetSecretGroupDecrypted(group string) (map[string]string, error) {
|
||||
docs, err := GetSecretGroup(group)
|
||||
// group. Used by the ESO read endpoint, which authenticates via a bearer
|
||||
// token rather than a session — org resolution for that path is a known gap,
|
||||
// tracked separately; the token is currently global rather than per-org.
|
||||
func GetSecretGroupDecrypted(orgID, group string) (map[string]string, error) {
|
||||
docs, err := GetSecretGroup(orgID, group)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -104,13 +107,43 @@ func GetSecretGroupDecrypted(group string) (map[string]string, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetSecretGroupDecryptedAny is the ESO-bearer-token read path: it has no
|
||||
// session/org context (the read token is currently global, not per-org), so
|
||||
// it looks up the group across all orgs. This mirrors pre-multi-tenant
|
||||
// behavior; scoping the ESO token to an org is tracked as a follow-up.
|
||||
func GetSecretGroupDecryptedAny(group string) (map[string]string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cursor, err := db.Col("secrets").Find(ctx, bson.M{"group": group},
|
||||
options.Find().SetSort(bson.D{{Key: "key", Value: 1}}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var docs []models.Secret
|
||||
if err := cursor.All(ctx, &docs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]string, len(docs))
|
||||
for _, doc := range docs {
|
||||
val, err := decryptString(doc.EncryptedValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decrypt %s/%s: %w", group, doc.Key, err)
|
||||
}
|
||||
result[doc.Key] = val
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// RevealSecret returns the decrypted value of a single key.
|
||||
func RevealSecret(group, key string) (string, error) {
|
||||
func RevealSecret(orgID, group, key string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var doc models.Secret
|
||||
err := db.Col("secrets").FindOne(ctx, bson.M{"group": group, "key": key}).Decode(&doc)
|
||||
err := db.Col("secrets").FindOne(ctx, bson.M{"org_id": orgID, "group": group, "key": key}).Decode(&doc)
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return "", fmt.Errorf("secret not found")
|
||||
}
|
||||
@@ -121,7 +154,7 @@ func RevealSecret(group, key string) (string, error) {
|
||||
}
|
||||
|
||||
// UpsertSecrets encrypts and writes each key/value pair into the group.
|
||||
func UpsertSecrets(group string, values map[string]string) error {
|
||||
func UpsertSecrets(orgID, group string, values map[string]string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -131,8 +164,9 @@ func UpsertSecrets(group string, values map[string]string) error {
|
||||
return fmt.Errorf("encrypt %s: %w", key, err)
|
||||
}
|
||||
_, err = db.Col("secrets").UpdateOne(ctx,
|
||||
bson.M{"group": group, "key": key},
|
||||
bson.M{"org_id": orgID, "group": group, "key": key},
|
||||
bson.M{"$set": bson.M{
|
||||
"org_id": orgID,
|
||||
"encrypted_value": encrypted,
|
||||
"updated_at": time.Now(),
|
||||
}},
|
||||
@@ -156,19 +190,19 @@ func SortedKeys(m map[string]string) []string {
|
||||
}
|
||||
|
||||
// DeleteSecret removes a single key from a group.
|
||||
func DeleteSecret(group, key string) error {
|
||||
func DeleteSecret(orgID, group, key string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := db.Col("secrets").DeleteOne(ctx, bson.M{"group": group, "key": key})
|
||||
_, err := db.Col("secrets").DeleteOne(ctx, bson.M{"org_id": orgID, "group": group, "key": key})
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteSecretGroup removes an entire group and all its keys.
|
||||
func DeleteSecretGroup(group string) error {
|
||||
func DeleteSecretGroup(orgID, group string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := db.Col("secrets").DeleteMany(ctx, bson.M{"group": group})
|
||||
_, err := db.Col("secrets").DeleteMany(ctx, bson.M{"org_id": orgID, "group": group})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -29,13 +29,14 @@ func HashToken(token string) string {
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func CreateServer() (*models.Server, string, error) {
|
||||
func CreateServer(orgID string) (*models.Server, string, error) {
|
||||
token, err := generateToken(32)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
expires := time.Now().Add(time.Hour)
|
||||
s := &models.Server{
|
||||
OrgID: orgID,
|
||||
ServerID: uuid.NewString(),
|
||||
PreRegToken: token,
|
||||
PreRegExpires: &expires,
|
||||
@@ -52,7 +53,22 @@ func CreateServer() (*models.Server, string, error) {
|
||||
return s, token, nil
|
||||
}
|
||||
|
||||
func GetServer(serverID string) (*models.Server, error) {
|
||||
// GetServer looks up a server scoped to an org (handler/session use).
|
||||
func GetServer(orgID, serverID string) (*models.Server, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var s models.Server
|
||||
err := db.Col("servers").FindOne(ctx, bson.M{"server_id": serverID, "org_id": orgID}).Decode(&s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// getServerByID looks up a server by its unique server_id with no org filter.
|
||||
// For agent/internal use only (e.g. workflow runner resolving org from a run).
|
||||
func getServerByID(serverID string) (*models.Server, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -214,12 +230,12 @@ func UpdateServerLastSeen(serverID, agentVersion string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func ListServers() ([]models.Server, error) {
|
||||
func ListServers(orgID string) ([]models.Server, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opts := options.Find().SetSort(bson.D{{Key: "created_at", Value: -1}})
|
||||
cursor, err := db.Col("servers").Find(ctx, bson.M{}, opts)
|
||||
cursor, err := db.Col("servers").Find(ctx, bson.M{"org_id": orgID}, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -232,11 +248,11 @@ func ListServers() ([]models.Server, error) {
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
func DeleteServer(serverID string) error {
|
||||
func DeleteServer(orgID, serverID string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := db.Col("servers").DeleteOne(ctx, bson.M{"server_id": serverID})
|
||||
_, err := db.Col("servers").DeleteOne(ctx, bson.M{"server_id": serverID, "org_id": orgID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -293,7 +309,7 @@ func MarkOfflineServers() error {
|
||||
}
|
||||
|
||||
for _, s := range goingOffline {
|
||||
LogEvent("server.offline", "system", s.ServerID, "", fmt.Sprintf("%s (%s) went offline", s.Hostname, s.IPAddress))
|
||||
LogEvent(s.OrgID, "server.offline", "system", s.ServerID, "", fmt.Sprintf("%s (%s) went offline", s.Hostname, s.IPAddress))
|
||||
if settings != nil && settings.Alerts.Enabled && settings.Alerts.WebhookURL != "" {
|
||||
go SendOfflineWebhook(settings.Alerts.WebhookURL, s.Hostname, s.ServerID, s.IPAddress)
|
||||
}
|
||||
|
||||
@@ -66,19 +66,19 @@ func ParseStepDoc(b []byte) (models.WorkflowStep, error) {
|
||||
}
|
||||
|
||||
// ImportStepToLibrary parses a doc and persists it as a new user library step.
|
||||
func ImportStepToLibrary(b []byte) (*models.WorkflowStep, error) {
|
||||
func ImportStepToLibrary(orgID string, b []byte) (*models.WorkflowStep, error) {
|
||||
s, err := ParseStepDoc(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return CreateStep(s)
|
||||
return CreateStep(orgID, s)
|
||||
}
|
||||
|
||||
// ExportStep loads a library step and marshals it to a portable doc.
|
||||
func ExportStep(stepID string) ([]byte, error) {
|
||||
func ExportStep(orgID, stepID string) ([]byte, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
s, err := getStep(ctx, stepID)
|
||||
s, err := getStep(ctx, orgID, stepID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ const stepDispatchGrace = 15 * time.Second
|
||||
|
||||
// TriggerWorkflow snapshots the workflow, creates a run doc, and starts a
|
||||
// background goroutine per target server (parallel fan-out). Returns run_id.
|
||||
func TriggerWorkflow(workflowID, actor string) (string, error) {
|
||||
wf, err := GetWorkflow(workflowID)
|
||||
func TriggerWorkflow(orgID, workflowID, actor string) (string, error) {
|
||||
wf, err := GetWorkflow(orgID, workflowID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -33,18 +33,19 @@ func TriggerWorkflow(workflowID, actor string) (string, error) {
|
||||
|
||||
// Reject a concurrent run of the same workflow.
|
||||
ctx, cancel := wfCtx()
|
||||
running := db.Col("workflow_runs").FindOne(ctx, bson.M{"workflow_id": workflowID, "status": "running"})
|
||||
running := db.Col("workflow_runs").FindOne(ctx, bson.M{"org_id": orgID, "workflow_id": workflowID, "status": "running"})
|
||||
cancel()
|
||||
if running.Err() == nil {
|
||||
return "", fmt.Errorf("workflow already has a run in progress")
|
||||
}
|
||||
|
||||
resolved, err := resolveSteps(wf)
|
||||
resolved, err := resolveSteps(orgID, wf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
run := models.WorkflowRun{
|
||||
OrgID: orgID,
|
||||
RunID: uuid.New().String(),
|
||||
WorkflowID: workflowID,
|
||||
Name: wf.Name,
|
||||
@@ -56,7 +57,7 @@ func TriggerWorkflow(workflowID, actor string) (string, error) {
|
||||
}
|
||||
for _, sid := range wf.TargetServerIDs {
|
||||
hostname := sid
|
||||
if s, e := GetServer(sid); e == nil {
|
||||
if s, e := getServerByID(sid); e == nil {
|
||||
hostname = s.Hostname
|
||||
}
|
||||
sr := models.ServerRun{ServerID: sid, Hostname: hostname, Status: "queued", RunEnv: map[string]string{}}
|
||||
@@ -78,7 +79,7 @@ func TriggerWorkflow(workflowID, actor string) (string, error) {
|
||||
|
||||
// resolveSteps freezes each workflow step ref into a ResolvedStep by loading the
|
||||
// library step and applying overrides.
|
||||
func resolveSteps(wf *models.Workflow) ([]models.ResolvedStep, error) {
|
||||
func resolveSteps(orgID string, wf *models.Workflow) ([]models.ResolvedStep, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
out := make([]models.ResolvedStep, 0, len(wf.Steps))
|
||||
@@ -87,7 +88,7 @@ func resolveSteps(wf *models.Workflow) ([]models.ResolvedStep, error) {
|
||||
out = append(out, resolveInlineStep(ref))
|
||||
continue
|
||||
}
|
||||
lib, err := getStep(ctx, ref.StepID)
|
||||
lib, err := getStep(ctx, orgID, ref.StepID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -158,14 +159,14 @@ func resolveInlineStep(ref models.WorkflowStepRef) models.ResolvedStep {
|
||||
|
||||
// executeRun fans out one goroutine per server run and waits for all to finish.
|
||||
func executeRun(runID string) {
|
||||
run, err := GetRun(runID)
|
||||
run, err := getRunByID(runID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
done := make(chan int, len(run.ServerRuns))
|
||||
for i := range run.ServerRuns {
|
||||
go func(idx int) {
|
||||
runServer(runID, idx, run.Steps, run.ServerRuns[idx].ServerID)
|
||||
runServer(run.OrgID, runID, idx, run.Steps, run.ServerRuns[idx].ServerID)
|
||||
done <- idx
|
||||
}(i)
|
||||
}
|
||||
@@ -174,7 +175,7 @@ func executeRun(runID string) {
|
||||
}
|
||||
|
||||
// Aggregate status.
|
||||
final, _ := GetRun(runID)
|
||||
final, _ := getRunByID(runID)
|
||||
status := "success"
|
||||
for _, sr := range final.ServerRuns {
|
||||
if sr.Status == "failed" {
|
||||
@@ -190,7 +191,7 @@ func executeRun(runID string) {
|
||||
|
||||
// runServer executes the resolved steps sequentially on one server, threading
|
||||
// output env forward and applying per-step failure policy.
|
||||
func runServer(runID string, srvIdx int, steps []models.ResolvedStep, serverID string) {
|
||||
func runServer(orgID, runID string, srvIdx int, steps []models.ResolvedStep, serverID string) {
|
||||
now := time.Now()
|
||||
setServerRun(runID, srvIdx, bson.M{"server_runs.$.status": "running", "server_runs.$.started_at": now})
|
||||
|
||||
@@ -218,7 +219,7 @@ func runServer(runID string, srvIdx int, steps []models.ResolvedStep, serverID s
|
||||
}
|
||||
|
||||
// Merge secrets into command env (kept out of persisted logs).
|
||||
secretVals := resolveSecrets(step.SecretRefs)
|
||||
secretVals := resolveSecrets(orgID, step.SecretRefs)
|
||||
for k, v := range secretVals {
|
||||
allSecrets[k] = v
|
||||
}
|
||||
@@ -366,7 +367,7 @@ func expandVars(v string, lookup map[string]string) string {
|
||||
})
|
||||
}
|
||||
|
||||
func resolveSecrets(refs []string) map[string]string {
|
||||
func resolveSecrets(orgID string, refs []string) map[string]string {
|
||||
out := map[string]string{}
|
||||
for _, ref := range refs {
|
||||
// ref format "group/KEY"; resolve via RevealSecret.
|
||||
@@ -374,7 +375,7 @@ func resolveSecrets(refs []string) map[string]string {
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
if v, err := RevealSecret(parts[0], parts[1]); err == nil {
|
||||
if v, err := RevealSecret(orgID, parts[0], parts[1]); err == nil {
|
||||
out[parts[1]] = v
|
||||
}
|
||||
}
|
||||
@@ -403,7 +404,7 @@ func setServerRun(runID string, srvIdx int, set bson.M) {
|
||||
|
||||
// serverIDAt returns the server_id at an index (positional operator needs a match).
|
||||
func serverIDAt(runID string, srvIdx int) string {
|
||||
r, err := GetRun(runID)
|
||||
r, err := getRunByID(runID)
|
||||
if err != nil || srvIdx >= len(r.ServerRuns) {
|
||||
return ""
|
||||
}
|
||||
@@ -467,7 +468,10 @@ func updateStep(runID, serverID string, order int, set bson.M) {
|
||||
|
||||
// ---- reads ----
|
||||
|
||||
func GetRun(runID string) (*models.WorkflowRun, error) {
|
||||
// getRunByID looks up a run by its unique run_id with no org filter. For
|
||||
// agent/internal run-execution use only (executeRun/runServer, etc.), which
|
||||
// don't have a session and instead resolve org from the run doc itself.
|
||||
func getRunByID(runID string) (*models.WorkflowRun, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
var r models.WorkflowRun
|
||||
@@ -478,10 +482,22 @@ func GetRun(runID string) (*models.WorkflowRun, error) {
|
||||
return &r, err
|
||||
}
|
||||
|
||||
func ListRuns(workflowID string, limit int64) ([]models.WorkflowRun, error) {
|
||||
// GetRun looks up a run scoped to an org (handler/session use).
|
||||
func GetRun(orgID, runID string) (*models.WorkflowRun, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
cur, err := db.Col("workflow_runs").Find(ctx, bson.M{"workflow_id": workflowID},
|
||||
var r models.WorkflowRun
|
||||
err := db.Col("workflow_runs").FindOne(ctx, bson.M{"run_id": runID, "org_id": orgID}).Decode(&r)
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, fmt.Errorf("run not found")
|
||||
}
|
||||
return &r, err
|
||||
}
|
||||
|
||||
func ListRuns(orgID, workflowID string, limit int64) ([]models.WorkflowRun, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
cur, err := db.Col("workflow_runs").Find(ctx, bson.M{"org_id": orgID, "workflow_id": workflowID},
|
||||
options.Find().SetSort(bson.D{{Key: "started_at", Value: -1}}).SetLimit(limit))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -494,12 +510,12 @@ func ListRuns(workflowID string, limit int64) ([]models.WorkflowRun, error) {
|
||||
return runs, nil
|
||||
}
|
||||
|
||||
func CancelRun(runID string) error {
|
||||
func CancelRun(orgID, runID string) error {
|
||||
now := time.Now()
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
_, err := db.Col("workflow_runs").UpdateOne(ctx,
|
||||
bson.M{"run_id": runID, "status": "running"},
|
||||
bson.M{"org_id": orgID, "run_id": runID, "status": "running"},
|
||||
bson.M{"$set": bson.M{"status": "cancelled", "finished_at": now}})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -45,10 +45,10 @@ func EnsureWorkflowIndexes() error {
|
||||
|
||||
// ---- Steps ----
|
||||
|
||||
func ListSteps() ([]models.WorkflowStep, error) {
|
||||
func ListSteps(orgID string) ([]models.WorkflowStep, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
cur, err := db.Col("workflow_steps").Find(ctx, bson.M{},
|
||||
cur, err := db.Col("workflow_steps").Find(ctx, bson.M{"org_id": orgID},
|
||||
options.Find().SetSort(bson.D{{Key: "name", Value: 1}}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -63,10 +63,10 @@ func ListSteps() ([]models.WorkflowStep, error) {
|
||||
|
||||
// StepUsageCounts returns, per library step_id, the number of distinct
|
||||
// workflows that reference it. Inline steps have no step_id and are ignored.
|
||||
func StepUsageCounts() (map[string]int, error) {
|
||||
func StepUsageCounts(orgID string) (map[string]int, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
cur, err := db.Col("workflows").Find(ctx, bson.M{})
|
||||
cur, err := db.Col("workflows").Find(ctx, bson.M{"org_id": orgID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -89,9 +89,10 @@ func StepUsageCounts() (map[string]int, error) {
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func CreateStep(s models.WorkflowStep) (*models.WorkflowStep, error) {
|
||||
func CreateStep(orgID string, s models.WorkflowStep) (*models.WorkflowStep, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
s.OrgID = orgID
|
||||
s.StepID = uuid.New().String()
|
||||
s.CreatedAt = time.Now()
|
||||
s.UpdatedAt = s.CreatedAt
|
||||
@@ -111,10 +112,10 @@ func CreateStep(s models.WorkflowStep) (*models.WorkflowStep, error) {
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func UpdateStep(stepID string, s models.WorkflowStep) error {
|
||||
func UpdateStep(orgID, stepID string, s models.WorkflowStep) error {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
_, err := db.Col("workflow_steps").UpdateOne(ctx, bson.M{"step_id": stepID}, bson.M{"$set": bson.M{
|
||||
_, err := db.Col("workflow_steps").UpdateOne(ctx, bson.M{"step_id": stepID, "org_id": orgID}, bson.M{"$set": bson.M{
|
||||
"name": s.Name,
|
||||
"description": s.Description,
|
||||
"interpreter": s.Interpreter,
|
||||
@@ -127,14 +128,14 @@ func UpdateStep(stepID string, s models.WorkflowStep) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteStep(stepID string) error {
|
||||
func DeleteStep(orgID, stepID string) error {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
if _, err := db.Col("workflow_steps").DeleteOne(ctx, bson.M{"step_id": stepID}); err != nil {
|
||||
if _, err := db.Col("workflow_steps").DeleteOne(ctx, bson.M{"step_id": stepID, "org_id": orgID}); err != nil {
|
||||
return err
|
||||
}
|
||||
// Cascade: remove this step from every workflow that references it, re-sequencing orders.
|
||||
cur, err := db.Col("workflows").Find(ctx, bson.M{"steps.step_id": stepID})
|
||||
cur, err := db.Col("workflows").Find(ctx, bson.M{"steps.step_id": stepID, "org_id": orgID})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -164,9 +165,9 @@ func DeleteStep(stepID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getStep(ctx context.Context, stepID string) (*models.WorkflowStep, error) {
|
||||
func getStep(ctx context.Context, orgID, stepID string) (*models.WorkflowStep, error) {
|
||||
var s models.WorkflowStep
|
||||
err := db.Col("workflow_steps").FindOne(ctx, bson.M{"step_id": stepID}).Decode(&s)
|
||||
err := db.Col("workflow_steps").FindOne(ctx, bson.M{"step_id": stepID, "org_id": orgID}).Decode(&s)
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, fmt.Errorf("step %s not found", stepID)
|
||||
}
|
||||
@@ -175,10 +176,10 @@ func getStep(ctx context.Context, stepID string) (*models.WorkflowStep, error) {
|
||||
|
||||
// ---- Workflows ----
|
||||
|
||||
func ListWorkflows() ([]models.Workflow, error) {
|
||||
func ListWorkflows(orgID string) ([]models.Workflow, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
cur, err := db.Col("workflows").Find(ctx, bson.M{},
|
||||
cur, err := db.Col("workflows").Find(ctx, bson.M{"org_id": orgID},
|
||||
options.Find().SetSort(bson.D{{Key: "name", Value: 1}}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -191,20 +192,21 @@ func ListWorkflows() ([]models.Workflow, error) {
|
||||
return wfs, nil
|
||||
}
|
||||
|
||||
func GetWorkflow(id string) (*models.Workflow, error) {
|
||||
func GetWorkflow(orgID, id string) (*models.Workflow, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
var w models.Workflow
|
||||
err := db.Col("workflows").FindOne(ctx, bson.M{"workflow_id": id}).Decode(&w)
|
||||
err := db.Col("workflows").FindOne(ctx, bson.M{"workflow_id": id, "org_id": orgID}).Decode(&w)
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, fmt.Errorf("workflow not found")
|
||||
}
|
||||
return &w, err
|
||||
}
|
||||
|
||||
func CreateWorkflow(w models.Workflow) (*models.Workflow, error) {
|
||||
func CreateWorkflow(orgID string, w models.Workflow) (*models.Workflow, error) {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
w.OrgID = orgID
|
||||
w.WorkflowID = uuid.New().String()
|
||||
w.CreatedAt = time.Now()
|
||||
w.UpdatedAt = w.CreatedAt
|
||||
@@ -224,14 +226,14 @@ func CreateWorkflow(w models.Workflow) (*models.Workflow, error) {
|
||||
return &w, nil
|
||||
}
|
||||
|
||||
func UpdateWorkflow(id string, w models.Workflow) error {
|
||||
func UpdateWorkflow(orgID, id string, w models.Workflow) error {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
if err := ValidateWorkflow(w); err != nil {
|
||||
return err
|
||||
}
|
||||
normalizeInlineSteps(&w)
|
||||
_, err := db.Col("workflows").UpdateOne(ctx, bson.M{"workflow_id": id}, bson.M{"$set": bson.M{
|
||||
_, err := db.Col("workflows").UpdateOne(ctx, bson.M{"workflow_id": id, "org_id": orgID}, bson.M{"$set": bson.M{
|
||||
"name": w.Name,
|
||||
"target_server_ids": w.TargetServerIDs,
|
||||
"steps": w.Steps,
|
||||
@@ -263,9 +265,9 @@ func normalizeInlineSteps(w *models.Workflow) {
|
||||
}
|
||||
}
|
||||
|
||||
func DeleteWorkflow(id string) error {
|
||||
func DeleteWorkflow(orgID, id string) error {
|
||||
ctx, cancel := wfCtx()
|
||||
defer cancel()
|
||||
_, err := db.Col("workflows").DeleteOne(ctx, bson.M{"workflow_id": id})
|
||||
_, err := db.Col("workflows").DeleteOne(ctx, bson.M{"workflow_id": id, "org_id": orgID})
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user