From 4f041d2f4b38b08830482cd1d0dbca3c22d833eb Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Fri, 24 Jul 2026 13:58:41 +0100 Subject: [PATCH] refactor(server): rename Org to Instance Adds migration 0004_org_to_instance, the ScopedCollections list, the AssertNoScopedCollectionMissed boot check, and moves EnsureAuthIndexes into its own file. Two ordering constraints the rename exposed, both now enforced and commented: - 0004 must run BEFORE EnsureAuthIndexes. The index builder creates instances.slug, which would create an empty instances collection and make 0004 refuse to rename orgs onto an existing target. - Migrations 0001 to 0003 run BEFORE 0004 and still read and write org_id, so they use a private legacyOrg struct rather than shared/models. --- server/cmd/main.go | 57 ++--- server/internal/api/channels.go | 10 +- server/internal/api/console.go | 35 ++- server/internal/api/handlers.go | 83 ++++---- server/internal/api/install_ps1.go | 5 +- server/internal/api/monitors.go | 18 +- server/internal/api/org.go | 42 ++-- server/internal/api/secrets.go | 59 ++---- server/internal/api/workflows.go | 75 ++++--- .../auth/{orghost.go => instancehost.go} | 16 +- server/internal/auth/local.go | 59 +++--- server/internal/auth/middleware.go | 10 +- server/internal/auth/oidc.go | 34 +-- server/internal/auth/session.go | 20 +- server/internal/checker/checker.go | 17 +- server/internal/grpc/codec.go | 3 +- server/internal/grpc/pb/vantage.pb.go | 48 ++--- server/internal/grpc/server.go | 20 +- server/internal/models/assignment.go | 2 +- server/internal/models/audit.go | 16 +- server/internal/models/channel.go | 20 +- server/internal/models/console_session.go | 22 +- server/internal/models/instance.go | 7 + server/internal/models/key.go | 4 +- server/internal/models/monitor.go | 24 +-- server/internal/models/org.go | 8 - server/internal/models/org_oidc.go | 2 +- server/internal/models/secret.go | 5 +- server/internal/models/server.go | 2 +- server/internal/models/workflow.go | 19 +- server/internal/monitorsched/scheduler.go | 6 +- server/internal/notify/dispatch.go | 7 - server/internal/notify/http.go | 1 - server/internal/notify/smtp.go | 8 - server/internal/notify/template.go | 5 - server/internal/services/audit.go | 20 +- server/internal/services/channels.go | 36 ++-- server/internal/services/console.go | 61 ++---- server/internal/services/coreindexes.go | 41 ++++ server/internal/services/crypto.go | 3 - server/internal/services/defaults.go | 20 +- server/internal/services/dispatch.go | 24 +-- .../{org_oidc.go => instance_oidc.go} | 18 +- .../services/{orgs.go => instances.go} | 55 +++-- server/internal/services/inventory.go | 2 - server/internal/services/keys.go | 67 +++--- server/internal/services/migrate.go | 53 ++--- server/internal/services/migrate_instance.go | 200 ++++++++++++++++++ server/internal/services/monitors.go | 96 ++++----- server/internal/services/secrets.go | 35 ++- server/internal/services/servers.go | 68 ++---- server/internal/services/settings.go | 44 ++-- server/internal/services/stepio.go | 14 +- server/internal/services/steplogs.go | 53 ++--- server/internal/services/stepresults.go | 6 - server/internal/services/stepscan.go | 7 +- server/internal/services/sync.go | 10 +- server/internal/services/users.go | 49 ++--- server/internal/services/validate.go | 1 - server/internal/services/workflow_runner.go | 38 ++-- server/internal/services/workflows.go | 72 +++---- 61 files changed, 895 insertions(+), 967 deletions(-) rename server/internal/auth/{orghost.go => instancehost.go} (90%) create mode 100644 server/internal/models/instance.go delete mode 100644 server/internal/models/org.go create mode 100644 server/internal/services/coreindexes.go rename server/internal/services/{org_oidc.go => instance_oidc.go} (61%) rename server/internal/services/{orgs.go => instances.go} (59%) create mode 100644 server/internal/services/migrate_instance.go diff --git a/server/cmd/main.go b/server/cmd/main.go index a775cb4..12c8a22 100644 --- a/server/cmd/main.go +++ b/server/cmd/main.go @@ -19,9 +19,6 @@ func main() { mongoURI := getEnv("MONGO_URI", "mongodb://localhost:27017") dbName := getEnv("MONGO_DB", "vantage") - - - if os.Getenv("GRPC_HOST") == "" { log.Fatal("GRPC_HOST is required (host:port agents dial for gRPC)") } @@ -31,19 +28,13 @@ func main() { } log.Println("connected to MongoDB") - - - - - if err := services.EnsureAuthIndexes(); err != nil { - log.Fatalf("failed to ensure auth indexes: %v", err) - } + // Migrations 0001 to 0003 still speak the pre-rename shape (orgs, org_id), + // so they must run before 0004 renames everything underneath them. if err := services.RunMigrations(); err != nil { log.Fatalf("migration failed: %v", err) } - - - + // 0002 must precede 0003: 0003 can create a "default" org, which pushes + // 0002 into its ambiguous multi-org branch. if err := services.MigrateSettingsOrg(); err != nil { log.Fatalf("settings org migration failed: %v", err) } @@ -51,13 +42,31 @@ func main() { log.Fatalf("missed org scope migration failed: %v", err) } + // 0004 renames orgs to instances. It must run BEFORE the index builders: + // EnsureAuthIndexes creates instances.slug, which would create an empty + // instances collection and make 0004 refuse to rename onto it. + migCtx, migCancel := context.WithTimeout(context.Background(), 10*time.Minute) + migErr := services.MigrateOrgToInstance(migCtx, db.Database) + migCancel() + if migErr != nil { + log.Fatalf("instance rename migration failed: %v", migErr) + } + + assertCtx, assertCancel := context.WithTimeout(context.Background(), 30*time.Second) + assertErr := services.AssertNoScopedCollectionMissed(assertCtx, db.Database) + assertCancel() + if assertErr != nil { + log.Fatalf("scoped collection check failed: %v", assertErr) + } + + if err := services.EnsureAuthIndexes(); err != nil { + log.Fatalf("failed to ensure auth indexes: %v", err) + } + if err := services.EnsureSecretIndexes(); err != nil { log.Printf("warning: failed to ensure secret indexes: %v", err) } - - - if err := services.EnsureSettingsIndexes(); err != nil { log.Fatalf("failed to ensure settings indexes: %v", err) } @@ -66,14 +75,14 @@ func main() { log.Printf("warning: failed to ensure workflow indexes: %v", err) } - if orgIDs, err := services.ListOrgIDs(); err != nil { - log.Printf("warning: failed to list orgs for default step seeding: %v", err) + if instanceIDs, err := services.ListInstanceIDs(); err != nil { + log.Printf("warning: failed to list instances for default step seeding: %v", err) } else { - for _, orgID := range orgIDs { - if created, updated, err := services.SeedDefaultSteps(orgID); err != nil { - log.Printf("warning: failed to seed default steps for org %s: %v", orgID, err) + for _, instanceID := range instanceIDs { + if created, updated, err := services.SeedDefaultSteps(instanceID); err != nil { + log.Printf("warning: failed to seed default steps for instance %s: %v", instanceID, err) } else { - log.Printf("default steps seeded for org %s: %d created, %d updated", orgID, created, updated) + log.Printf("default steps seeded for instance %s: %d created, %d updated", instanceID, created, updated) } } } @@ -86,7 +95,6 @@ func main() { } log.Println("connected to Redis") - go func() { ticker := time.NewTicker(2 * time.Minute) defer ticker.Stop() @@ -97,17 +105,14 @@ func main() { } }() - go func() { if err := grpcserver.StartGRPC(9090); err != nil { log.Fatalf("gRPC server error: %v", err) } }() - monitorsched.Start(context.Background()) - r := gin.New() r.Use(gin.Recovery()) r.Use(gin.LoggerWithConfig(gin.LoggerConfig{SkipPaths: []string{"/api/console/tunnel"}})) diff --git a/server/internal/api/channels.go b/server/internal/api/channels.go index 790edc5..6b0114c 100644 --- a/server/internal/api/channels.go +++ b/server/internal/api/channels.go @@ -19,7 +19,7 @@ func registerChannelRoutes(g *gin.RouterGroup) { } func listChannels(c *gin.Context) { - channels, err := services.ListChannels(auth.OrgID(c)) + channels, err := services.ListChannels(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -37,7 +37,7 @@ func createChannel(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "name and type are required"}) return } - created, err := services.CreateChannel(auth.OrgID(c), &ch) + created, err := services.CreateChannel(auth.InstanceID(c), &ch) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -73,7 +73,7 @@ func updateChannel(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) return } - if err := services.UpdateChannel(auth.OrgID(c), c.Param("id"), upd); err != nil { + if err := services.UpdateChannel(auth.InstanceID(c), c.Param("id"), upd); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -81,7 +81,7 @@ func updateChannel(c *gin.Context) { } func deleteChannel(c *gin.Context) { - if err := services.DeleteChannel(auth.OrgID(c), c.Param("id")); err != nil { + if err := services.DeleteChannel(auth.InstanceID(c), c.Param("id")); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -89,7 +89,7 @@ func deleteChannel(c *gin.Context) { } func testChannel(c *gin.Context) { - if err := services.TestChannel(auth.OrgID(c), c.Param("id")); err != nil { + if err := services.TestChannel(auth.InstanceID(c), c.Param("id")); err != nil { c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()}) return } diff --git a/server/internal/api/console.go b/server/internal/api/console.go index 735d1e8..3aedbb3 100644 --- a/server/internal/api/console.go +++ b/server/internal/api/console.go @@ -13,9 +13,6 @@ import ( "github.com/wwt/guac" ) - - - func consoleConnect(c *gin.Context) { var body struct { ServerID string `json:"server_id" binding:"required"` @@ -30,13 +27,13 @@ func consoleConnect(c *gin.Context) { return } - srv, err := services.GetServer(auth.OrgID(c), body.ServerID) + srv, err := services.GetServer(auth.InstanceID(c), body.ServerID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) return } - sess, err := services.CreateConsoleSession(auth.OrgID(c), body.ServerID, body.Protocol, body.KeyID, actorFromCtx(c), c.ClientIP()) + sess, err := services.CreateConsoleSession(auth.InstanceID(c), body.ServerID, body.Protocol, body.KeyID, actorFromCtx(c), c.ClientIP()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -48,20 +45,20 @@ func consoleConnect(c *gin.Context) { } if (body.Protocol == "rdp" || body.Protocol == "vnc") && (body.RDPUsername != "" || body.RDPPassword != "") { - if err := services.StashConsoleRDPCreds(auth.OrgID(c), sess.SessionID, body.RDPUsername, body.RDPPassword); err != nil { + if err := services.StashConsoleRDPCreds(auth.InstanceID(c), sess.SessionID, body.RDPUsername, body.RDPPassword); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } if body.Protocol == "ssh" { - if err := services.SetConsoleSSHUser(auth.OrgID(c), sess.SessionID, body.SSHUsername); err != nil { + if err := services.SetConsoleSSHUser(auth.InstanceID(c), sess.SessionID, body.SSHUsername); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } - services.LogEvent(auth.OrgID(c), "console.opened", actorFromCtx(c), srv.ServerID, "", + services.LogEvent(auth.InstanceID(c), "console.opened", actorFromCtx(c), srv.ServerID, "", "console session opened ("+body.Protocol+")") c.JSON(http.StatusOK, gin.H{ @@ -71,8 +68,6 @@ func consoleConnect(c *gin.Context) { }) } - - func queryIntDefault(r *http.Request, key string, def int) int { v, err := strconv.Atoi(r.URL.Query().Get(key)) if err != nil || v <= 0 { @@ -81,7 +76,6 @@ func queryIntDefault(r *http.Request, key string, def int) int { return v } - func consoleTunnel(c *gin.Context) { token := c.Query("token") sessionID, err := services.VerifySessionToken(token) @@ -89,36 +83,32 @@ func consoleTunnel(c *gin.Context) { c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) return } - orgID := auth.OrgID(c) - sess, err := services.GetConsoleSession(orgID, sessionID) + instanceID := auth.InstanceID(c) + sess, err := services.GetConsoleSession(instanceID, sessionID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "session not found"}) return } - - if actor := actorFromCtx(c); actor != sess.User { c.JSON(http.StatusForbidden, gin.H{"error": "session belongs to another user"}) return } - - if err := services.ConsumeSessionToken(orgID, sessionID); err != nil { + if err := services.ConsumeSessionToken(instanceID, sessionID); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "token already used"}) return } - srv, err := services.GetServer(auth.OrgID(c), sess.ServerID) + srv, err := services.GetServer(auth.InstanceID(c), sess.ServerID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) return } - var privKey, passphrase string if sess.Protocol == "ssh" && sess.KeyID != "" { - privKey, err = services.GetPrivateKey(auth.OrgID(c), sess.KeyID) + privKey, err = services.GetPrivateKey(auth.InstanceID(c), sess.KeyID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "selected key has no private material"}) return @@ -128,7 +118,7 @@ func consoleTunnel(c *gin.Context) { var rdpUser, rdpPass string if sess.Protocol == "rdp" || sess.Protocol == "vnc" { - rdpUser, rdpPass, err = services.ConsumeConsoleRDPCreds(orgID, sessionID) + rdpUser, rdpPass, err = services.ConsumeConsoleRDPCreds(instanceID, sessionID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "could not load credentials"}) return @@ -145,7 +135,6 @@ func consoleTunnel(c *gin.Context) { guacdAddr = "guacd:4822" } - connect := func(r *http.Request) (guac.Tunnel, error) { config := guac.NewGuacamoleConfiguration() config.Protocol = gp.Protocol @@ -173,7 +162,7 @@ func consoleTunnel(c *gin.Context) { wsServer := guac.NewWebsocketServer(connect) wsServer.OnDisconnect = func(id string, r *http.Request, t guac.Tunnel) { - _ = services.EndConsoleSession(orgID, sessionID) + _ = services.EndConsoleSession(instanceID, sessionID) } wsServer.ServeHTTP(c.Writer, c.Request) } diff --git a/server/internal/api/handlers.go b/server/internal/api/handlers.go index 80b9e61..9157947 100644 --- a/server/internal/api/handlers.go +++ b/server/internal/api/handlers.go @@ -27,7 +27,6 @@ func RegisterRoutes(r *gin.Engine) { r.GET("/api/secrets/:group/values", secretsReadAuth(), esoGetGroup) - r.GET("/auth/bootstrap-status", auth.HandleBootstrapStatus) r.POST("/auth/bootstrap", auth.HandleBootstrap) r.POST("/auth/login", auth.HandleLocalLogin) @@ -36,7 +35,6 @@ func RegisterRoutes(r *gin.Engine) { r.GET("/auth/oidc/start", auth.HandleOIDCStart) r.GET("/auth/oidc/callback", auth.HandleOIDCCallback) - apiGroup := r.Group("/api") apiGroup.Use(auth.Middleware()) { @@ -85,21 +83,21 @@ func RegisterRoutes(r *gin.Engine) { registerMonitorRoutes(apiGroup) registerChannelRoutes(apiGroup) - org := apiGroup.Group("/org") - org.Use(auth.RequireRole("owner", "admin")) + instance := apiGroup.Group("/instance") + instance.Use(auth.RequireRole("owner", "admin")) { - org.GET("/users", listOrgUsers) - org.POST("/users", createOrgUser) - org.PUT("/users/:id/role", updateOrgUserRole) - org.DELETE("/users/:id", deleteOrgUser) - org.GET("/oidc", getOrgOIDC) - org.PUT("/oidc", putOrgOIDC) + instance.GET("/users", listInstanceUsers) + instance.POST("/users", createInstanceUser) + instance.PUT("/users/:id/role", updateInstanceUserRole) + instance.DELETE("/users/:id", deleteInstanceUser) + instance.GET("/oidc", getInstanceOIDC) + instance.PUT("/oidc", putInstanceOIDC) } } } func listServers(c *gin.Context) { - servers, err := services.ListServers(auth.OrgID(c)) + servers, err := services.ListServers(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -108,7 +106,7 @@ func listServers(c *gin.Context) { } func createServer(c *gin.Context) { - s, token, err := services.CreateServer(auth.OrgID(c)) + s, token, err := services.CreateServer(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -121,12 +119,12 @@ func createServer(c *gin.Context) { } func newServer(c *gin.Context) { - s, token, err := services.CreateServer(auth.OrgID(c)) + s, token, err := services.CreateServer(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "server.created", actorFromCtx(c), s.ServerID, "", "pre-registration token issued") + services.LogEvent(auth.InstanceID(c), "server.created", actorFromCtx(c), s.ServerID, "", "pre-registration token issued") giteaHost := os.Getenv("GITEA_HOST") if giteaHost == "" { @@ -155,15 +153,14 @@ func newServer(c *gin.Context) { func getServer(c *gin.Context) { id := c.Param("id") - s, err := services.GetServer(auth.OrgID(c), id) + s, err := services.GetServer(auth.InstanceID(c), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) return } - assignments, _ := services.GetAssignmentsWithKeysForServer(auth.OrgID(c), id) + assignments, _ := services.GetAssignmentsWithKeysForServer(auth.InstanceID(c), id) - type serverResponse struct { *models.Server Keys interface{} `json:"keys"` @@ -176,8 +173,8 @@ func getServer(c *gin.Context) { func deleteServer(c *gin.Context) { id := c.Param("id") - s, _ := services.GetServer(auth.OrgID(c), id) - if err := services.DeleteServer(auth.OrgID(c), id); err != nil { + s, _ := services.GetServer(auth.InstanceID(c), id) + if err := services.DeleteServer(auth.InstanceID(c), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -185,7 +182,7 @@ func deleteServer(c *gin.Context) { if s != nil { hostname = s.Hostname } - services.LogEvent(auth.OrgID(c), "server.deleted", actorFromCtx(c), id, "", fmt.Sprintf("server %s deleted", hostname)) + services.LogEvent(auth.InstanceID(c), "server.deleted", actorFromCtx(c), id, "", fmt.Sprintf("server %s deleted", hostname)) c.JSON(http.StatusOK, gin.H{"deleted": true}) } @@ -204,7 +201,7 @@ func generateKey(c *gin.Context) { body.Label = "generated" } - s, err := services.GetServer(auth.OrgID(c), id) + s, err := services.GetServer(auth.InstanceID(c), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) return @@ -222,7 +219,7 @@ func generateKey(c *gin.Context) { return } - services.LogEvent(auth.OrgID(c), "key.generation_dispatched", actorFromCtx(c), s.ServerID, "", fmt.Sprintf("key generation dispatched (label=%s type=%s)", body.Label, body.KeyType)) + services.LogEvent(auth.InstanceID(c), "key.generation_dispatched", actorFromCtx(c), s.ServerID, "", fmt.Sprintf("key generation dispatched (label=%s type=%s)", body.Label, body.KeyType)) c.JSON(http.StatusAccepted, gin.H{ "message": "key generation command sent to agent", "command_id": cmdID, @@ -231,7 +228,7 @@ func generateKey(c *gin.Context) { } func listKeys(c *gin.Context) { - keys, err := services.ListKeys(auth.OrgID(c)) + keys, err := services.ListKeys(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -251,18 +248,18 @@ func createKey(c *gin.Context) { return } - key, err := services.CreateKey(auth.OrgID(c), body.Label, body.PublicKey, "uploaded", "", body.PrivateKey, body.Passphrase) + key, err := services.CreateKey(auth.InstanceID(c), body.Label, body.PublicKey, "uploaded", "", body.PrivateKey, body.Passphrase) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "key.uploaded", actorFromCtx(c), "", key.KeyID, fmt.Sprintf("key '%s' uploaded", key.Label)) + services.LogEvent(auth.InstanceID(c), "key.uploaded", actorFromCtx(c), "", key.KeyID, fmt.Sprintf("key '%s' uploaded", key.Label)) c.JSON(http.StatusCreated, key) } func getPrivateKey(c *gin.Context) { id := c.Param("id") - plaintext, err := services.GetPrivateKey(auth.OrgID(c), id) + plaintext, err := services.GetPrivateKey(auth.InstanceID(c), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return @@ -272,13 +269,13 @@ func getPrivateKey(c *gin.Context) { func getKey(c *gin.Context) { id := c.Param("id") - key, err := services.GetKey(auth.OrgID(c), id) + key, err := services.GetKey(auth.InstanceID(c), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "key not found"}) return } - assignments, _ := services.GetAssignmentsWithServers(auth.OrgID(c), id) + assignments, _ := services.GetAssignmentsWithServers(auth.InstanceID(c), id) type keyResponse struct { *models.Key @@ -292,8 +289,8 @@ func getKey(c *gin.Context) { func deleteKey(c *gin.Context) { id := c.Param("id") - k, _ := services.GetKey(auth.OrgID(c), id) - if err := services.DeleteKey(auth.OrgID(c), id); err != nil { + k, _ := services.GetKey(auth.InstanceID(c), id) + if err := services.DeleteKey(auth.InstanceID(c), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -301,7 +298,7 @@ func deleteKey(c *gin.Context) { if k != nil { label = k.Label } - services.LogEvent(auth.OrgID(c), "key.deleted", actorFromCtx(c), "", id, fmt.Sprintf("key '%s' deleted", label)) + services.LogEvent(auth.InstanceID(c), "key.deleted", actorFromCtx(c), "", id, fmt.Sprintf("key '%s' deleted", label)) c.JSON(http.StatusOK, gin.H{"deleted": true}) } @@ -315,12 +312,12 @@ func assignKey(c *gin.Context) { return } - a, err := services.AssignKey(auth.OrgID(c), keyID, body.ServerID) + a, err := services.AssignKey(auth.InstanceID(c), keyID, body.ServerID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "key.assigned", actorFromCtx(c), body.ServerID, keyID, fmt.Sprintf("key %s assigned to server %s", keyID, body.ServerID)) + services.LogEvent(auth.InstanceID(c), "key.assigned", actorFromCtx(c), body.ServerID, keyID, fmt.Sprintf("key %s assigned to server %s", keyID, body.ServerID)) c.JSON(http.StatusCreated, a) } @@ -328,11 +325,11 @@ func revokeAssignment(c *gin.Context) { keyID := c.Param("id") serverID := c.Param("serverId") - if err := services.RevokeAssignment(auth.OrgID(c), keyID, serverID); err != nil { + if err := services.RevokeAssignment(auth.InstanceID(c), keyID, serverID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "key.revoked", actorFromCtx(c), serverID, keyID, fmt.Sprintf("key %s revoked from server %s", keyID, serverID)) + services.LogEvent(auth.InstanceID(c), "key.revoked", actorFromCtx(c), serverID, keyID, fmt.Sprintf("key %s revoked from server %s", keyID, serverID)) c.JSON(http.StatusOK, gin.H{"revoked": true}) } @@ -347,7 +344,7 @@ func getLatestAgentVersion(c *gin.Context) { func updateAgent(c *gin.Context) { id := c.Param("id") - s, err := services.GetServer(auth.OrgID(c), id) + s, err := services.GetServer(auth.InstanceID(c), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) return @@ -358,7 +355,7 @@ func updateAgent(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "agent.update_dispatched", actorFromCtx(c), s.ServerID, "", fmt.Sprintf("agent update dispatched to %s (version %s)", s.Hostname, version)) + services.LogEvent(auth.InstanceID(c), "agent.update_dispatched", actorFromCtx(c), s.ServerID, "", fmt.Sprintf("agent update dispatched to %s (version %s)", s.Hostname, version)) c.JSON(http.StatusAccepted, gin.H{ "message": "update command sent to agent", "version": version, @@ -367,7 +364,7 @@ func updateAgent(c *gin.Context) { func applyUpdates(c *gin.Context) { id := c.Param("id") - s, err := services.GetServer(auth.OrgID(c), id) + s, err := services.GetServer(auth.InstanceID(c), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "server not found"}) return @@ -377,7 +374,7 @@ func applyUpdates(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "updates.applied", actorFromCtx(c), s.ServerID, "", fmt.Sprintf("package update command dispatched to %s", s.Hostname)) + services.LogEvent(auth.InstanceID(c), "updates.applied", actorFromCtx(c), s.ServerID, "", fmt.Sprintf("package update command dispatched to %s", s.Hostname)) c.JSON(http.StatusAccepted, gin.H{"message": "apply updates command sent to agent"}) } @@ -444,7 +441,7 @@ func listAuditEvents(c *gin.Context) { limit = n } } - events, err := services.ListAuditEvents(auth.OrgID(c), limit) + events, err := services.ListAuditEvents(auth.InstanceID(c), limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -453,7 +450,7 @@ func listAuditEvents(c *gin.Context) { } func getSettings(c *gin.Context) { - s, err := services.GetSettings(auth.OrgID(c)) + s, err := services.GetSettings(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -471,11 +468,11 @@ func saveSettings(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := services.SaveSettings(auth.OrgID(c), body.Alerts, body.Email, body.WorkflowLogRetentionDays); err != nil { + if err := services.SaveSettings(auth.InstanceID(c), body.Alerts, body.Email, body.WorkflowLogRetentionDays); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "settings.updated", actorFromCtx(c), "", "", "alert settings updated") + services.LogEvent(auth.InstanceID(c), "settings.updated", actorFromCtx(c), "", "", "alert settings updated") c.JSON(http.StatusOK, gin.H{"saved": true}) } diff --git a/server/internal/api/install_ps1.go b/server/internal/api/install_ps1.go index 81a1fab..2a97652 100644 --- a/server/internal/api/install_ps1.go +++ b/server/internal/api/install_ps1.go @@ -16,7 +16,7 @@ func handleInstallScriptWindows(c *gin.Context) { if giteaHost == "" { giteaHost = "gitea.example.com" } - + grpcHost := os.Getenv("GRPC_HOST") script := fmt.Sprintf( @@ -50,9 +50,6 @@ func handleInstallScriptWindows(c *gin.Context) { c.String(http.StatusOK, script) } - - - func handleUpdateScriptWindows(c *gin.Context) { giteaHost := os.Getenv("GITEA_HOST") if giteaHost == "" { diff --git a/server/internal/api/monitors.go b/server/internal/api/monitors.go index 0a63c6d..78010e5 100644 --- a/server/internal/api/monitors.go +++ b/server/internal/api/monitors.go @@ -22,7 +22,7 @@ func registerMonitorRoutes(g *gin.RouterGroup) { } func listMonitors(c *gin.Context) { - monitors, err := services.ListMonitors(auth.OrgID(c)) + monitors, err := services.ListMonitors(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -40,7 +40,7 @@ func createMonitor(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "name and type are required"}) return } - created, err := services.CreateMonitor(auth.OrgID(c), &m) + created, err := services.CreateMonitor(auth.InstanceID(c), &m) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -49,7 +49,7 @@ func createMonitor(c *gin.Context) { } func getMonitor(c *gin.Context) { - m, err := services.GetMonitor(auth.OrgID(c), c.Param("id")) + m, err := services.GetMonitor(auth.InstanceID(c), c.Param("id")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -105,7 +105,7 @@ func updateMonitor(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"}) return } - if err := services.UpdateMonitor(auth.OrgID(c), c.Param("id"), upd); err != nil { + if err := services.UpdateMonitor(auth.InstanceID(c), c.Param("id"), upd); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -113,7 +113,7 @@ func updateMonitor(c *gin.Context) { } func deleteMonitor(c *gin.Context) { - if err := services.DeleteMonitor(auth.OrgID(c), c.Param("id")); err != nil { + if err := services.DeleteMonitor(auth.InstanceID(c), c.Param("id")); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } @@ -121,7 +121,7 @@ func deleteMonitor(c *gin.Context) { } func getMonitorIncidents(c *gin.Context) { - m, err := services.GetMonitor(auth.OrgID(c), c.Param("id")) + m, err := services.GetMonitor(auth.InstanceID(c), c.Param("id")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -130,7 +130,7 @@ func getMonitorIncidents(c *gin.Context) { c.JSON(http.StatusNotFound, gin.H{"error": "monitor not found"}) return } - incidents, err := services.ListIncidents(auth.OrgID(c), c.Param("id"), 50) + incidents, err := services.ListIncidents(auth.InstanceID(c), c.Param("id"), 50) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -139,7 +139,7 @@ func getMonitorIncidents(c *gin.Context) { } func getMonitorUptime(c *gin.Context) { - m, err := services.GetMonitor(auth.OrgID(c), c.Param("id")) + m, err := services.GetMonitor(auth.InstanceID(c), c.Param("id")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -149,7 +149,7 @@ func getMonitorUptime(c *gin.Context) { return } since := time.Now().Add(-30 * 24 * time.Hour) - rollups, err := services.UptimeRollups(auth.OrgID(c), c.Param("id"), since) + rollups, err := services.UptimeRollups(auth.InstanceID(c), c.Param("id"), since) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return diff --git a/server/internal/api/org.go b/server/internal/api/org.go index e0d66e3..4eb9643 100644 --- a/server/internal/api/org.go +++ b/server/internal/api/org.go @@ -10,8 +10,8 @@ import ( "github.com/mrhid6/vantage/server/internal/services" ) -func listOrgUsers(c *gin.Context) { - users, err := services.ListUsers(auth.OrgID(c)) +func listInstanceUsers(c *gin.Context) { + users, err := services.ListUsers(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -19,14 +19,11 @@ func listOrgUsers(c *gin.Context) { c.JSON(http.StatusOK, users) } - - - func actorMayGrantOwner(c *gin.Context) bool { return auth.Role(c) == models.RoleOwner } -func createOrgUser(c *gin.Context) { +func createInstanceUser(c *gin.Context) { var body struct { Email string `json:"email"` Password string `json:"password"` @@ -47,7 +44,7 @@ func createOrgUser(c *gin.Context) { c.JSON(http.StatusForbidden, gin.H{"error": "only an owner can create another owner"}) return } - u, err := services.CreateUser(auth.OrgID(c), body.Email, body.Password, body.Role, "local") + u, err := services.CreateUser(auth.InstanceID(c), body.Email, body.Password, body.Role, "local") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -55,7 +52,7 @@ func createOrgUser(c *gin.Context) { c.JSON(http.StatusCreated, u) } -func updateOrgUserRole(c *gin.Context) { +func updateInstanceUserRole(c *gin.Context) { var body struct { Role string `json:"role"` } @@ -68,12 +65,12 @@ func updateOrgUserRole(c *gin.Context) { return } - orgID, targetID := auth.OrgID(c), c.Param("id") + instanceID, targetID := auth.InstanceID(c), c.Param("id") if targetID == auth.UserID(c) { c.JSON(http.StatusForbidden, gin.H{"error": "you cannot change your own role"}) return } - target, err := services.GetUserInOrg(orgID, targetID) + target, err := services.GetUserInInstance(instanceID, targetID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return @@ -83,20 +80,20 @@ func updateOrgUserRole(c *gin.Context) { return } - if err := services.UpdateUserRole(orgID, targetID, body.Role); err != nil { + if err := services.UpdateUserRole(instanceID, targetID, body.Role); err != nil { c.JSON(orgUserErrStatus(err), gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } -func deleteOrgUser(c *gin.Context) { - orgID, targetID := auth.OrgID(c), c.Param("id") +func deleteInstanceUser(c *gin.Context) { + instanceID, targetID := auth.InstanceID(c), c.Param("id") if targetID == auth.UserID(c) { c.JSON(http.StatusForbidden, gin.H{"error": "you cannot remove your own account"}) return } - target, err := services.GetUserInOrg(orgID, targetID) + target, err := services.GetUserInInstance(instanceID, targetID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "user not found"}) return @@ -106,7 +103,7 @@ func deleteOrgUser(c *gin.Context) { return } - if err := services.DeleteUser(orgID, targetID); err != nil { + if err := services.DeleteUser(instanceID, targetID); err != nil { c.JSON(orgUserErrStatus(err), gin.H{"error": err.Error()}) return } @@ -120,16 +117,15 @@ func orgUserErrStatus(err error) int { return http.StatusInternalServerError } -func getOrgOIDC(c *gin.Context) { - cfg, err := services.GetOrgOIDC(auth.OrgID(c)) +func getInstanceOIDC(c *gin.Context) { + cfg, err := services.GetInstanceOIDC(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusOK, gin.H{"enabled": false, "client_secret_set": false}) return } - - + c.JSON(http.StatusOK, gin.H{ - "org_id": cfg.OrgID, + "instance_id": cfg.InstanceID, "issuer": cfg.Issuer, "client_id": cfg.ClientID, "enabled": cfg.Enabled, @@ -138,7 +134,7 @@ func getOrgOIDC(c *gin.Context) { }) } -func putOrgOIDC(c *gin.Context) { +func putInstanceOIDC(c *gin.Context) { var body struct { Issuer string `json:"issuer"` ClientID string `json:"client_id"` @@ -149,11 +145,11 @@ func putOrgOIDC(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := services.SaveOrgOIDC(auth.OrgID(c), body.Issuer, body.ClientID, body.ClientSecret, body.Enabled); err != nil { + if err := services.SaveOrgOIDC(auth.InstanceID(c), body.Issuer, body.ClientID, body.ClientSecret, body.Enabled); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - auth.EvictOIDCProvider(auth.OrgID(c)) + auth.EvictOIDCProvider(auth.InstanceID(c)) c.JSON(http.StatusOK, gin.H{"saved": true}) } diff --git a/server/internal/api/secrets.go b/server/internal/api/secrets.go index a79378a..e65dc69 100644 --- a/server/internal/api/secrets.go +++ b/server/internal/api/secrets.go @@ -11,22 +11,13 @@ import ( "github.com/mrhid6/vantage/server/internal/services" ) - - var groupNamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) func validName(s string) bool { return s != "" && len(s) <= 128 && groupNamePattern.MatchString(s) } - -const ctxSecretsOrgKey = "km_secrets_org" - - - - - - +const ctxSecretsInstanceKey = "km_secrets_instance" func secretsReadAuth() gin.HandlerFunc { return func(c *gin.Context) { @@ -36,29 +27,26 @@ func secretsReadAuth() gin.HandlerFunc { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"}) return } - orgID, ok := services.ResolveSecretsReadToken(authHeader[len(prefix):]) + instanceID, ok := services.ResolveSecretsReadToken(authHeader[len(prefix):]) if !ok { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) return } - c.Set(ctxSecretsOrgKey, orgID) + c.Set(ctxSecretsInstanceKey, instanceID) c.Next() } } - - - func esoGetGroup(c *gin.Context) { group := c.Param("group") - - orgID := c.GetString(ctxSecretsOrgKey) - if orgID == "" { - + + instanceID := c.GetString(ctxSecretsInstanceKey) + if instanceID == "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"}) return } - values, err := services.GetSecretGroupDecrypted(orgID, group) + values, err := services.GetSecretGroupDecrypted(instanceID, group) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "store error"}) return @@ -71,7 +59,7 @@ func esoGetGroup(c *gin.Context) { } func listSecretGroups(c *gin.Context) { - groups, err := services.ListSecretGroups(auth.OrgID(c)) + groups, err := services.ListSecretGroups(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -79,8 +67,6 @@ func listSecretGroups(c *gin.Context) { c.JSON(http.StatusOK, groups) } - - func createSecretGroup(c *gin.Context) { var body struct { Group string `json:"group" binding:"required"` @@ -104,17 +90,17 @@ func createSecretGroup(c *gin.Context) { return } } - if err := services.UpsertSecrets(auth.OrgID(c), body.Group, body.Values); err != nil { + if err := services.UpsertSecrets(auth.InstanceID(c), body.Group, body.Values); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "secret.updated", actorFromCtx(c), "", "", fmt.Sprintf("group '%s' created with keys: %s", body.Group, strings.Join(services.SortedKeys(body.Values), ", "))) + services.LogEvent(auth.InstanceID(c), "secret.updated", actorFromCtx(c), "", "", fmt.Sprintf("group '%s' created with keys: %s", body.Group, strings.Join(services.SortedKeys(body.Values), ", "))) c.JSON(http.StatusCreated, gin.H{"group": body.Group}) } func getSecretGroup(c *gin.Context) { group := c.Param("group") - secrets, err := services.GetSecretGroup(auth.OrgID(c), group) + secrets, err := services.GetSecretGroup(auth.InstanceID(c), group) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -126,7 +112,6 @@ func getSecretGroup(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"group": group, "secrets": secrets}) } - func putSecretGroup(c *gin.Context) { group := c.Param("group") if !validName(group) { @@ -148,11 +133,11 @@ func putSecretGroup(c *gin.Context) { return } } - if err := services.UpsertSecrets(auth.OrgID(c), group, values); err != nil { + if err := services.UpsertSecrets(auth.InstanceID(c), group, values); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "secret.updated", actorFromCtx(c), "", "", fmt.Sprintf("group '%s' keys updated: %s", group, strings.Join(services.SortedKeys(values), ", "))) + services.LogEvent(auth.InstanceID(c), "secret.updated", actorFromCtx(c), "", "", fmt.Sprintf("group '%s' keys updated: %s", group, strings.Join(services.SortedKeys(values), ", "))) c.JSON(http.StatusOK, gin.H{"saved": true}) } @@ -165,42 +150,42 @@ func revealSecret(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - value, err := services.RevealSecret(auth.OrgID(c), group, body.Key) + value, err := services.RevealSecret(auth.InstanceID(c), group, body.Key) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "secret.revealed", actorFromCtx(c), "", "", fmt.Sprintf("value of '%s/%s' revealed", group, body.Key)) + services.LogEvent(auth.InstanceID(c), "secret.revealed", actorFromCtx(c), "", "", fmt.Sprintf("value of '%s/%s' revealed", group, body.Key)) c.JSON(http.StatusOK, gin.H{"value": value}) } func deleteSecretKey(c *gin.Context) { group := c.Param("group") key := c.Param("key") - if err := services.DeleteSecret(auth.OrgID(c), group, key); err != nil { + if err := services.DeleteSecret(auth.InstanceID(c), group, key); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "secret.deleted", actorFromCtx(c), "", "", fmt.Sprintf("key '%s' deleted from group '%s'", key, group)) + services.LogEvent(auth.InstanceID(c), "secret.deleted", actorFromCtx(c), "", "", fmt.Sprintf("key '%s' deleted from group '%s'", key, group)) c.JSON(http.StatusOK, gin.H{"deleted": true}) } func deleteSecretGroup(c *gin.Context) { group := c.Param("group") - if err := services.DeleteSecretGroup(auth.OrgID(c), group); err != nil { + if err := services.DeleteSecretGroup(auth.InstanceID(c), group); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "secretgroup.deleted", actorFromCtx(c), "", "", fmt.Sprintf("group '%s' deleted", group)) + services.LogEvent(auth.InstanceID(c), "secretgroup.deleted", actorFromCtx(c), "", "", fmt.Sprintf("group '%s' deleted", group)) c.JSON(http.StatusOK, gin.H{"deleted": true}) } func rotateSecretsToken(c *gin.Context) { - token, err := services.RotateSecretsReadToken(auth.OrgID(c)) + token, err := services.RotateSecretsReadToken(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "secrets.token_rotated", actorFromCtx(c), "", "", "ESO read token rotated") + services.LogEvent(auth.InstanceID(c), "secrets.token_rotated", actorFromCtx(c), "", "", "ESO read token rotated") c.JSON(http.StatusOK, gin.H{"token": token}) } diff --git a/server/internal/api/workflows.go b/server/internal/api/workflows.go index 5f37a57..2040a5c 100644 --- a/server/internal/api/workflows.go +++ b/server/internal/api/workflows.go @@ -81,7 +81,7 @@ func streamServerRunLog(c *gin.Context) { sendNew := func() { f, err := os.Open(path) if err != nil { - return + return } defer f.Close() if _, err := f.Seek(offset, 0); err != nil { @@ -94,7 +94,7 @@ func streamServerRunLog(c *gin.Context) { break } offset += int64(n) - + for _, line := range splitSSE(buf[:n]) { _, _ = c.Writer.WriteString("data: " + line + "\n") } @@ -106,11 +106,11 @@ func streamServerRunLog(c *gin.Context) { ctx := c.Request.Context() ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() - orgID := auth.OrgID(c) + instanceID := auth.InstanceID(c) for { sendNew() - if serverRunTerminal(orgID, runID, serverID) { - sendNew() + if serverRunTerminal(instanceID, runID, serverID) { + sendNew() _, _ = c.Writer.WriteString("event: done\ndata: end\n\n") flusher.Flush() return @@ -123,9 +123,8 @@ func streamServerRunLog(c *gin.Context) { } } - -func serverRunTerminal(orgID, runID, serverID string) bool { - r, err := services.GetRun(orgID, runID) +func serverRunTerminal(instanceID, runID, serverID string) bool { + r, err := services.GetRun(instanceID, runID) if err != nil { return true } @@ -141,15 +140,13 @@ func serverRunTerminal(orgID, runID, serverID string) bool { return true } - - func splitSSE(b []byte) []string { s := strings.ReplaceAll(string(b), "\r", "") return strings.Split(s, "\n") } func listSteps(c *gin.Context) { - steps, err := services.ListSteps(auth.OrgID(c)) + steps, err := services.ListSteps(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -158,7 +155,7 @@ func listSteps(c *gin.Context) { } func stepUsage(c *gin.Context) { - counts, err := services.StepUsageCounts(auth.OrgID(c)) + counts, err := services.StepUsageCounts(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -172,12 +169,12 @@ func createStep(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - out, err := services.CreateStep(auth.OrgID(c), s) + out, err := services.CreateStep(auth.InstanceID(c), s) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "workflow.step_created", actorFromCtx(c), "", out.StepID, fmt.Sprintf("step '%s' created", out.Name)) + services.LogEvent(auth.InstanceID(c), "workflow.step_created", actorFromCtx(c), "", out.StepID, fmt.Sprintf("step '%s' created", out.Name)) c.JSON(http.StatusCreated, out) } @@ -187,25 +184,25 @@ func updateStep(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := services.UpdateStep(auth.OrgID(c), c.Param("id"), s); err != nil { + if err := services.UpdateStep(auth.InstanceID(c), c.Param("id"), s); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "workflow.step_updated", actorFromCtx(c), "", c.Param("id"), "step updated") + services.LogEvent(auth.InstanceID(c), "workflow.step_updated", actorFromCtx(c), "", c.Param("id"), "step updated") c.JSON(http.StatusOK, gin.H{"updated": true}) } func deleteStep(c *gin.Context) { - if err := services.DeleteStep(auth.OrgID(c), c.Param("id")); err != nil { + if err := services.DeleteStep(auth.InstanceID(c), c.Param("id")); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "workflow.step_deleted", actorFromCtx(c), "", c.Param("id"), "step deleted") + services.LogEvent(auth.InstanceID(c), "workflow.step_deleted", actorFromCtx(c), "", c.Param("id"), "step deleted") c.JSON(http.StatusOK, gin.H{"deleted": true}) } func exportStep(c *gin.Context) { - b, err := services.ExportStep(auth.OrgID(c), c.Param("id")) + b, err := services.ExportStep(auth.InstanceID(c), c.Param("id")) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return @@ -215,16 +212,16 @@ func exportStep(c *gin.Context) { } func seedDefaults(c *gin.Context) { - created, updated, err := services.SeedDefaultSteps(auth.OrgID(c)) + created, updated, err := services.SeedDefaultSteps(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "workflow.defaults_synced", actorFromCtx(c), "", "", fmt.Sprintf("default steps synced: %d created, %d updated", created, updated)) + services.LogEvent(auth.InstanceID(c), "workflow.defaults_synced", actorFromCtx(c), "", "", fmt.Sprintf("default steps synced: %d created, %d updated", created, updated)) c.JSON(http.StatusOK, gin.H{"created": created, "updated": updated}) } -const maxStepBodyBytes = 1 << 20 +const maxStepBodyBytes = 1 << 20 func importStep(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxStepBodyBytes) @@ -233,12 +230,12 @@ func importStep(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - out, err := services.ImportStepToLibrary(auth.OrgID(c), body) + out, err := services.ImportStepToLibrary(auth.InstanceID(c), body) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "workflow.step_imported", actorFromCtx(c), "", out.StepID, fmt.Sprintf("step '%s' imported", out.Name)) + services.LogEvent(auth.InstanceID(c), "workflow.step_imported", actorFromCtx(c), "", out.StepID, fmt.Sprintf("step '%s' imported", out.Name)) c.JSON(http.StatusCreated, out) } @@ -258,7 +255,7 @@ func parseStep(c *gin.Context) { } func listWorkflows(c *gin.Context) { - wfs, err := services.ListWorkflows(auth.OrgID(c)) + wfs, err := services.ListWorkflows(auth.InstanceID(c)) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -272,17 +269,17 @@ func createWorkflow(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - out, err := services.CreateWorkflow(auth.OrgID(c), w) + out, err := services.CreateWorkflow(auth.InstanceID(c), w) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "workflow.created", actorFromCtx(c), "", out.WorkflowID, fmt.Sprintf("workflow '%s' created", out.Name)) + services.LogEvent(auth.InstanceID(c), "workflow.created", actorFromCtx(c), "", out.WorkflowID, fmt.Sprintf("workflow '%s' created", out.Name)) c.JSON(http.StatusCreated, out) } func getWorkflow(c *gin.Context) { - w, err := services.GetWorkflow(auth.OrgID(c), c.Param("id")) + w, err := services.GetWorkflow(auth.InstanceID(c), c.Param("id")) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return @@ -296,12 +293,12 @@ func updateWorkflow(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - if err := services.UpdateWorkflow(auth.OrgID(c), c.Param("id"), w); err != nil { + if err := services.UpdateWorkflow(auth.InstanceID(c), c.Param("id"), w); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "workflow.updated", actorFromCtx(c), "", c.Param("id"), "workflow updated") - updated, err := services.GetWorkflow(auth.OrgID(c), c.Param("id")) + services.LogEvent(auth.InstanceID(c), "workflow.updated", actorFromCtx(c), "", c.Param("id"), "workflow updated") + updated, err := services.GetWorkflow(auth.InstanceID(c), c.Param("id")) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -310,21 +307,21 @@ func updateWorkflow(c *gin.Context) { } func deleteWorkflow(c *gin.Context) { - if err := services.DeleteWorkflow(auth.OrgID(c), c.Param("id")); err != nil { + if err := services.DeleteWorkflow(auth.InstanceID(c), c.Param("id")); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "workflow.deleted", actorFromCtx(c), "", c.Param("id"), "workflow deleted") + services.LogEvent(auth.InstanceID(c), "workflow.deleted", actorFromCtx(c), "", c.Param("id"), "workflow deleted") c.JSON(http.StatusOK, gin.H{"deleted": true}) } func runWorkflow(c *gin.Context) { - runID, err := services.TriggerWorkflow(auth.OrgID(c), c.Param("id"), actorFromCtx(c)) + runID, err := services.TriggerWorkflow(auth.InstanceID(c), c.Param("id"), actorFromCtx(c)) if err != nil { c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "workflow.run_triggered", actorFromCtx(c), "", c.Param("id"), fmt.Sprintf("run %s triggered", runID)) + services.LogEvent(auth.InstanceID(c), "workflow.run_triggered", actorFromCtx(c), "", c.Param("id"), fmt.Sprintf("run %s triggered", runID)) c.JSON(http.StatusAccepted, gin.H{"run_id": runID}) } @@ -335,7 +332,7 @@ func listWorkflowRuns(c *gin.Context) { limit = n } } - runs, err := services.ListRuns(auth.OrgID(c), c.Param("id"), limit) + runs, err := services.ListRuns(auth.InstanceID(c), c.Param("id"), limit) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return @@ -344,7 +341,7 @@ func listWorkflowRuns(c *gin.Context) { } func getRun(c *gin.Context) { - r, err := services.GetRun(auth.OrgID(c), c.Param("runId")) + r, err := services.GetRun(auth.InstanceID(c), c.Param("runId")) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return @@ -353,10 +350,10 @@ func getRun(c *gin.Context) { } func cancelRun(c *gin.Context) { - if err := services.CancelRun(auth.OrgID(c), c.Param("runId")); err != nil { + if err := services.CancelRun(auth.InstanceID(c), c.Param("runId")); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - services.LogEvent(auth.OrgID(c), "workflow.run_cancelled", actorFromCtx(c), "", c.Param("runId"), "run cancelled") + services.LogEvent(auth.InstanceID(c), "workflow.run_cancelled", actorFromCtx(c), "", c.Param("runId"), "run cancelled") c.JSON(http.StatusOK, gin.H{"cancelled": true}) } diff --git a/server/internal/auth/orghost.go b/server/internal/auth/instancehost.go similarity index 90% rename from server/internal/auth/orghost.go rename to server/internal/auth/instancehost.go index 4281855..3853e19 100644 --- a/server/internal/auth/orghost.go +++ b/server/internal/auth/instancehost.go @@ -12,7 +12,7 @@ import ( ) type cachedOrg struct { - org *models.Org + org *models.Instance at time.Time } @@ -23,9 +23,6 @@ var ( const orgCacheTTL = 60 * time.Second - - - func appRootLabel() string { if v := os.Getenv("APP_ROOT_LABEL"); v != "" { return strings.ToLower(v) @@ -33,15 +30,13 @@ func appRootLabel() string { return "vantage" } - - func hostSlug(host string) string { host = strings.ToLower(host) if i := strings.IndexByte(host, ':'); i >= 0 { host = host[:i] } root := appRootLabel() - + parts := strings.Split(host, ".") if len(parts) < 3 { return "" @@ -55,7 +50,7 @@ func hostSlug(host string) string { return parts[0] } -func OrgFromHost(c *gin.Context) (*models.Org, bool) { +func InstanceFromHost(c *gin.Context) (*models.Instance, bool) { slug := hostSlug(c.Request.Host) if slug == "" { return nil, false @@ -67,10 +62,9 @@ func OrgFromHost(c *gin.Context) (*models.Org, bool) { } orgCacheMu.Unlock() - org, err := services.GetOrgBySlug(slug) + org, err := services.GetInstanceBySlug(slug) if err != nil || org == nil { - - + return nil, false } orgCacheMu.Lock() diff --git a/server/internal/auth/local.go b/server/internal/auth/local.go index 8b5df21..1069132 100644 --- a/server/internal/auth/local.go +++ b/server/internal/auth/local.go @@ -37,7 +37,7 @@ func HandleLocalLogin(c *gin.Context) { return } sessionID, err := SaveSession(c.Request.Context(), &Session{ - UserID: u.UserID, OrgID: u.OrgID, Role: u.Role, Email: u.Email, + UserID: u.UserID, InstanceID: u.InstanceID, Role: u.Role, Email: u.Email, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"}) @@ -50,13 +50,13 @@ func HandleLocalLogin(c *gin.Context) { func HandleBootstrapStatus(c *gin.Context) { var ( - n int64 - err error - orgName string + n int64 + err error + instName string ) - if org, ok := OrgFromHost(c); ok { - n, err = services.CountOrgUsers(org.OrgID) - orgName = org.Name + if inst, ok := InstanceFromHost(c); ok { + n, err = services.CountInstanceUsers(inst.InstanceID) + instName = inst.Name } else { n, err = services.CountUsers() } @@ -64,12 +64,9 @@ func HandleBootstrapStatus(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - c.JSON(http.StatusOK, gin.H{"needs_setup": n == 0, "org_name": orgName}) + c.JSON(http.StatusOK, gin.H{"needs_setup": n == 0, "instance_name": instName}) } - - - func HandleBootstrap(c *gin.Context) { n, err := services.CountUsers() if err != nil { @@ -81,34 +78,34 @@ func HandleBootstrap(c *gin.Context) { return } var body struct { - OrgName string `json:"org_name"` - Email string `json:"email"` - Password string `json:"password"` + InstanceName string `json:"instance_name"` + Email string `json:"email"` + Password string `json:"password"` } - if err := c.ShouldBindJSON(&body); err != nil || body.OrgName == "" || body.Email == "" || len(body.Password) < 8 { + if err := c.ShouldBindJSON(&body); err != nil || body.InstanceName == "" || body.Email == "" || len(body.Password) < 8 { c.JSON(http.StatusBadRequest, gin.H{"error": "org_name, email, and password (>=8 chars) required"}) return } - orgCount, err := services.CountOrgs() + orgCount, err := services.CountInstances() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } - var org *models.Org + var inst *models.Instance switch orgCount { case 0: - org, err = services.CreateOrg(body.OrgName) + inst, err = services.CreateInstance(body.InstanceName) case 1: - var existing *models.Org - existing, err = services.FirstOrg() + var existing *models.Instance + existing, err = services.FirstInstance() if err == nil { - org, err = services.AdoptOrg(existing.OrgID, body.OrgName) + inst, err = services.AdoptInstance(existing.InstanceID, body.InstanceName) } default: c.JSON(http.StatusConflict, gin.H{"error": fmt.Sprintf( "cannot bootstrap: %d organizations already exist but no users do; "+ - "create the owner against the intended org rather than through setup, "+ + "create the owner against the intended inst rather than through setup, "+ "or remove the unintended orgs and retry", orgCount)}) return } @@ -116,20 +113,20 @@ func HandleBootstrap(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - u, err := services.CreateUser(org.OrgID, body.Email, body.Password, "owner", "local") + u, err := services.CreateUser(inst.InstanceID, body.Email, body.Password, "owner", "local") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } sessionID, err := SaveSession(c.Request.Context(), &Session{ - UserID: u.UserID, OrgID: u.OrgID, Role: u.Role, Email: u.Email, + UserID: u.UserID, InstanceID: u.InstanceID, Role: u.Role, Email: u.Email, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"}) return } SetSessionCookie(c, sessionID) - c.JSON(http.StatusCreated, gin.H{"org": org, "slug": org.Slug}) + c.JSON(http.StatusCreated, gin.H{"instance": inst, "slug": inst.Slug}) } func HandleMe(c *gin.Context) { @@ -143,14 +140,12 @@ func HandleMe(c *gin.Context) { c.JSON(http.StatusUnauthorized, gin.H{"error": "session expired"}) return } - - - - if hostOrg, ok := OrgFromHost(c); ok && hostOrg.OrgID != sess.OrgID { - c.JSON(http.StatusForbidden, gin.H{"error": "org host mismatch"}) + + if hostInstance, ok := InstanceFromHost(c); ok && hostInstance.InstanceID != sess.InstanceID { + c.JSON(http.StatusForbidden, gin.H{"error": "instance host mismatch"}) return } - org, _ := services.GetOrg(sess.OrgID) - c.JSON(http.StatusOK, gin.H{"user": sess, "org": org}) + inst, _ := services.GetInstance(sess.InstanceID) + c.JSON(http.StatusOK, gin.H{"user": sess, "instance": inst}) } diff --git a/server/internal/auth/middleware.go b/server/internal/auth/middleware.go index 1ea963b..ac2022d 100644 --- a/server/internal/auth/middleware.go +++ b/server/internal/auth/middleware.go @@ -14,9 +14,9 @@ func GetSessionFromContext(c *gin.Context) *Session { return sess } -func OrgID(c *gin.Context) string { +func InstanceID(c *gin.Context) string { if s := GetSessionFromContext(c); s != nil { - return s.OrgID + return s.InstanceID } return "" } @@ -62,15 +62,15 @@ func Middleware() gin.HandlerFunc { return } - if sess.OrgID == "" { + if sess.InstanceID == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "session has no organization"}) return } c.Set(ctxSessionKey, sess) - if hostOrg, ok := OrgFromHost(c); ok && hostOrg.OrgID != sess.OrgID { - c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "org host mismatch"}) + if hostInstance, ok := InstanceFromHost(c); ok && hostInstance.InstanceID != sess.InstanceID { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "instance host mismatch"}) return } diff --git a/server/internal/auth/oidc.go b/server/internal/auth/oidc.go index f4f8481..add8191 100644 --- a/server/internal/auth/oidc.go +++ b/server/internal/auth/oidc.go @@ -18,9 +18,9 @@ var ( provCache = map[string]*oidc.Provider{} ) -func EvictOIDCProvider(orgID string) { +func EvictOIDCProvider(instanceID string) { provMu.Lock() - delete(provCache, orgID) + delete(provCache, instanceID) provMu.Unlock() } @@ -32,17 +32,17 @@ func redirectURL(c *gin.Context) string { return fmt.Sprintf("%s://%s/auth/oidc/callback", scheme, c.Request.Host) } -func providerForOrg(ctx context.Context, c *gin.Context, orgID string) (*oidc.Provider, *oauth2.Config, error) { - cfg, err := services.GetOrgOIDC(orgID) +func providerForOrg(ctx context.Context, c *gin.Context, instanceID string) (*oidc.Provider, *oauth2.Config, error) { + cfg, err := services.GetInstanceOIDC(instanceID) if err != nil || !cfg.Enabled { - return nil, nil, fmt.Errorf("org SSO not configured") + return nil, nil, fmt.Errorf("inst SSO not configured") } - secret, err := services.GetOrgOIDCSecret(orgID) + secret, err := services.GetInstanceOIDCSecret(instanceID) if err != nil { return nil, nil, err } provMu.Lock() - p := provCache[orgID] + p := provCache[instanceID] provMu.Unlock() if p == nil { p, err = oidc.NewProvider(ctx, cfg.Issuer) @@ -50,7 +50,7 @@ func providerForOrg(ctx context.Context, c *gin.Context, orgID string) (*oidc.Pr return nil, nil, err } provMu.Lock() - provCache[orgID] = p + provCache[instanceID] = p provMu.Unlock() } return p, &oauth2.Config{ @@ -61,13 +61,13 @@ func providerForOrg(ctx context.Context, c *gin.Context, orgID string) (*oidc.Pr } func HandleOIDCStart(c *gin.Context) { - org, ok := OrgFromHost(c) + inst, ok := InstanceFromHost(c) if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": "unknown organization host"}) return } ctx := c.Request.Context() - _, oauthCfg, err := providerForOrg(ctx, c, org.OrgID) + _, oauthCfg, err := providerForOrg(ctx, c, inst.InstanceID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -77,7 +77,7 @@ func HandleOIDCStart(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{"error": "state gen failed"}) return } - if err := SaveStateOrg(ctx, state, org.OrgID); err != nil { + if err := SaveStateOrg(ctx, state, inst.InstanceID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "state save failed"}) return } @@ -86,12 +86,12 @@ func HandleOIDCStart(c *gin.Context) { func HandleOIDCCallback(c *gin.Context) { ctx := c.Request.Context() - orgID, ok := ConsumeStateOrg(ctx, c.Query("state")) + instanceID, ok := ConsumeStateOrg(ctx, c.Query("state")) if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid state"}) return } - provider, oauthCfg, err := providerForOrg(ctx, c, orgID) + provider, oauthCfg, err := providerForOrg(ctx, c, instanceID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return @@ -123,19 +123,19 @@ func HandleOIDCCallback(c *gin.Context) { email := strings.ToLower(claims.Email) u, err := services.GetUserByEmail(email) if err != nil { - - u, err = services.CreateUser(orgID, email, "", "member", "oidc") + + u, err = services.CreateUser(instanceID, email, "", "member", "oidc") if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "provisioning failed"}) return } - } else if u.OrgID != orgID { + } else if u.InstanceID != instanceID { c.JSON(http.StatusForbidden, gin.H{"error": "email belongs to a different organization"}) return } sessionID, err := SaveSession(ctx, &Session{ - UserID: u.UserID, OrgID: u.OrgID, Role: u.Role, Email: u.Email, Name: claims.Name, + UserID: u.UserID, InstanceID: u.InstanceID, Role: u.Role, Email: u.Email, Name: claims.Name, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "session save failed"}) diff --git a/server/internal/auth/session.go b/server/internal/auth/session.go index b72a736..e0ecf0a 100644 --- a/server/internal/auth/session.go +++ b/server/internal/auth/session.go @@ -16,11 +16,11 @@ const sessionPrefix = "km:session:" const statePrefix = "km:state:" type Session struct { - UserID string `json:"user_id"` - OrgID string `json:"org_id"` - Role string `json:"role"` - Email string `json:"email"` - Name string `json:"name"` + UserID string `json:"user_id"` + InstanceID string `json:"instance_id"` + Role string `json:"role"` + Email string `json:"email"` + Name string `json:"name"` } var rdb *redis.Client @@ -71,14 +71,14 @@ func DeleteSession(ctx context.Context, id string) error { return rdb.Del(ctx, sessionPrefix+id).Err() } -func SaveStateOrg(ctx context.Context, state, orgID string) error { - return rdb.Set(ctx, statePrefix+state, orgID, 10*time.Minute).Err() +func SaveStateOrg(ctx context.Context, state, instanceID string) error { + return rdb.Set(ctx, statePrefix+state, instanceID, 10*time.Minute).Err() } func ConsumeStateOrg(ctx context.Context, state string) (string, bool) { - orgID, err := rdb.GetDel(ctx, statePrefix+state).Result() - if err != nil || orgID == "" { + instanceID, err := rdb.GetDel(ctx, statePrefix+state).Result() + if err != nil || instanceID == "" { return "", false } - return orgID, true + return instanceID, true } diff --git a/server/internal/checker/checker.go b/server/internal/checker/checker.go index c2d8eba..475845d 100644 --- a/server/internal/checker/checker.go +++ b/server/internal/checker/checker.go @@ -12,7 +12,6 @@ import ( "time" ) - const ( TypeHTTP = "http" TypeTCP = "tcp" @@ -20,7 +19,6 @@ const ( TypeTLS = "tls" ) - type Spec struct { Type string URL string @@ -30,11 +28,10 @@ type Spec struct { ExpectedStatus int Keyword string TLSWarnDays int - Insecure bool + Insecure bool TimeoutSec int } - type Result struct { Up bool LatencyMs int @@ -50,7 +47,6 @@ func (s Spec) timeout() time.Duration { return time.Duration(t) * time.Second } - func Run(ctx context.Context, s Spec) Result { switch s.Type { case TypeHTTP: @@ -77,7 +73,7 @@ func runHTTP(ctx context.Context, s Spec) Result { } client := &http.Client{Timeout: s.timeout()} if s.Insecure { - client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + client.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} } start := time.Now() req, err := http.NewRequestWithContext(ctx, method, s.URL, nil) @@ -156,9 +152,6 @@ func runTLS(ctx context.Context, s Spec) Result { func msSince(t time.Time) int { return int(time.Since(t).Milliseconds()) } - - - func runICMP(ctx context.Context, s Spec) Result { dst, err := net.ResolveIPAddr("ip4", s.Host) if err != nil { @@ -188,18 +181,18 @@ func runICMP(ctx context.Context, s Spec) Result { if err != nil { return Result{LatencyMs: msSince(start), Message: "no reply"} } - + if n < 28 || peer.String() != dst.String() { continue } - if reply[20] == 0 { + if reply[20] == 0 { return Result{Up: true, LatencyMs: msSince(start)} } } } func icmpEcho(id, seq int) []byte { - + b := []byte{8, 0, 0, 0, byte(id >> 8), byte(id), byte(seq >> 8), byte(seq)} cs := icmpChecksum(b) b[2] = byte(cs >> 8) diff --git a/server/internal/grpc/codec.go b/server/internal/grpc/codec.go index 8113132..fea87ec 100644 --- a/server/internal/grpc/codec.go +++ b/server/internal/grpc/codec.go @@ -4,7 +4,6 @@ import ( "encoding/json" ) - type JSONCodec struct{} func (JSONCodec) Marshal(v interface{}) ([]byte, error) { @@ -16,5 +15,5 @@ func (JSONCodec) Unmarshal(data []byte, v interface{}) error { } func (JSONCodec) Name() string { - return "proto" + return "proto" } diff --git a/server/internal/grpc/pb/vantage.pb.go b/server/internal/grpc/pb/vantage.pb.go index 399cc52..24a0053 100644 --- a/server/internal/grpc/pb/vantage.pb.go +++ b/server/internal/grpc/pb/vantage.pb.go @@ -1,6 +1,3 @@ - - - package pb import ( @@ -11,8 +8,6 @@ import ( "google.golang.org/grpc/status" ) - - type RegisterRequest struct { ServerId string `json:"server_id"` PreRegToken string `json:"pre_reg_token"` @@ -47,8 +42,6 @@ type UploadKeyResponse struct { KeyId string `json:"key_id"` } - - type PackageUpdate struct { Name string `json:"name"` CurrentVersion string `json:"current_version,omitempty"` @@ -63,8 +56,6 @@ type ReportUpdatesRequest struct { type ReportUpdatesResponse struct{} - - type CPUReport struct { Model string `json:"model,omitempty"` Cores int `json:"cores,omitempty"` @@ -95,8 +86,6 @@ type InventoryReport struct { } type InventoryReportResponse struct{} - - type MonitorSpec struct { MonitorId string `json:"monitor_id"` Type string `json:"type"` @@ -119,11 +108,11 @@ type SyncMonitorsResponse struct { Monitors []MonitorSpec `json:"monitors,omitempty"` } type CheckResult struct { - MonitorId string `json:"monitor_id"` - Up bool `json:"up"` - LatencyMs int `json:"latency_ms"` - Message string `json:"message,omitempty"` - CertExpiryUnix int64 `json:"cert_expiry_unix,omitempty"` + MonitorId string `json:"monitor_id"` + Up bool `json:"up"` + LatencyMs int `json:"latency_ms"` + Message string `json:"message,omitempty"` + CertExpiryUnix int64 `json:"cert_expiry_unix,omitempty"` } type ReportChecksRequest struct { ServerId string `json:"server_id"` @@ -144,8 +133,6 @@ type ServerCommand struct { CleanupWorkspace *CleanupWorkspaceCmd `json:"cleanup_workspace,omitempty"` } - - type CleanupWorkspaceCmd struct { WorkspaceId string `json:"workspace_id"` } @@ -168,12 +155,12 @@ type GenerateKeyCmd struct { } type AgentMessage struct { - ServerId string `json:"server_id"` - AgentToken string `json:"agent_token"` - Ready *AgentReady `json:"ready,omitempty"` - Result *CommandResult `json:"result,omitempty"` - StepResult *StepResult `json:"step_result,omitempty"` - StepOutput *StepOutputChunk `json:"step_output,omitempty"` + ServerId string `json:"server_id"` + AgentToken string `json:"agent_token"` + Ready *AgentReady `json:"ready,omitempty"` + Result *CommandResult `json:"result,omitempty"` + StepResult *StepResult `json:"step_result,omitempty"` + StepOutput *StepOutputChunk `json:"step_output,omitempty"` } type AgentReady struct{} @@ -189,8 +176,7 @@ type RunStepCmd struct { Script string `json:"script"` Env map[string]string `json:"env,omitempty"` TimeoutSeconds int `json:"timeout_seconds,omitempty"` - - + WorkspaceId string `json:"workspace_id,omitempty"` } @@ -209,8 +195,6 @@ type StepOutputChunk struct { Eof bool `json:"eof,omitempty"` } - - type Vantage_CommandStreamServer interface { Send(*ServerCommand) error Recv() (*AgentMessage, error) @@ -233,8 +217,6 @@ func (s *keyManagerCommandStreamServer) Recv() (*AgentMessage, error) { return m, nil } - - type Vantage_CommandStreamClient interface { Send(*AgentMessage) error Recv() (*ServerCommand, error) @@ -257,8 +239,6 @@ func (c *vantageCommandStreamClient) Recv() (*ServerCommand, error) { return m, nil } - - type VantageServer interface { Register(context.Context, *RegisterRequest) (*RegisterResponse, error) SyncKeys(context.Context, *SyncRequest) (*SyncResponse, error) @@ -304,8 +284,6 @@ func (UnimplementedVantageServer) CommandStream(Vantage_CommandStreamServer) err return status.Errorf(codes.Unimplemented, "method CommandStream not implemented") } - - type VantageClient interface { Register(ctx context.Context, in *RegisterRequest, opts ...grpc.CallOption) (*RegisterResponse, error) SyncKeys(ctx context.Context, in *SyncRequest, opts ...grpc.CallOption) (*SyncResponse, error) @@ -389,8 +367,6 @@ func (c *keyManagerClient) CommandStream(ctx context.Context, opts ...grpc.CallO return &vantageCommandStreamClient{stream}, nil } - - func RegisterVantageServer(s grpc.ServiceRegistrar, srv VantageServer) { s.RegisterService(&Vantage_ServiceDesc, srv) } diff --git a/server/internal/grpc/server.go b/server/internal/grpc/server.go index 39972a3..75b1bbd 100644 --- a/server/internal/grpc/server.go +++ b/server/internal/grpc/server.go @@ -62,14 +62,12 @@ func (s *vantageServer) UploadGeneratedKey(ctx context.Context, req *pb.UploadKe return nil, status.Errorf(codes.Unauthenticated, "invalid agent token") } - - key, err := services.CreateKey(srv.OrgID, req.Label, req.PublicKey, "generated", srv.ServerID, req.PrivateKey, "") + key, err := services.CreateKey(srv.InstanceID, req.Label, req.PublicKey, "generated", srv.ServerID, req.PrivateKey, "") if err != nil { return nil, status.Errorf(codes.Internal, "failed to store key: %v", err) } - - if _, err := services.AssignKey(srv.OrgID, key.KeyID, srv.ServerID); err != nil { + if _, err := services.AssignKey(srv.InstanceID, key.KeyID, srv.ServerID); err != nil { log.Printf("failed to auto-assign generated key: %v", err) } @@ -112,7 +110,7 @@ func (s *vantageServer) SyncMonitors(ctx context.Context, req *pb.SyncMonitorsRe if err != nil { return nil, status.Errorf(codes.Unauthenticated, "invalid agent token") } - monitors, err := services.ListMonitorsForRunner(srv.OrgID, srv.ServerID) + monitors, err := services.ListMonitorsForRunner(srv.InstanceID, srv.ServerID) if err != nil { return nil, status.Errorf(codes.Internal, "list monitors") } @@ -147,7 +145,7 @@ func (s *vantageServer) ReportChecks(ctx context.Context, req *pb.ReportChecksRe t := time.Unix(r.CertExpiryUnix, 0) res.CertExpiry = &t } - if err := services.IngestResult(srv.OrgID, srv.ServerID, r.MonitorId, res); err != nil { + if err := services.IngestResult(srv.InstanceID, srv.ServerID, r.MonitorId, res); err != nil { log.Printf("ingest check %s: %v", r.MonitorId, err) } } @@ -155,7 +153,7 @@ func (s *vantageServer) ReportChecks(ctx context.Context, req *pb.ReportChecksRe } func (s *vantageServer) CommandStream(stream pb.Vantage_CommandStreamServer) error { - + msg, err := stream.Recv() if err != nil { return status.Errorf(codes.InvalidArgument, "expected initial auth message: %v", err) @@ -176,8 +174,6 @@ func (s *vantageServer) CommandStream(stream pb.Vantage_CommandStreamServer) err log.Printf("agent %s connected command stream", srv.ServerID) defer log.Printf("agent %s disconnected command stream", srv.ServerID) - - go func() { for { m, err := stream.Recv() @@ -224,15 +220,13 @@ func StartGRPC(port int) error { } s := grpc.NewServer( - - + grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{ MinTime: 20 * time.Second, PermitWithoutStream: false, }), grpc.KeepaliveParams(keepalive.ServerParameters{ - - + Time: 45 * time.Second, Timeout: 10 * time.Second, }), diff --git a/server/internal/models/assignment.go b/server/internal/models/assignment.go index a0c3cd4..60e7614 100644 --- a/server/internal/models/assignment.go +++ b/server/internal/models/assignment.go @@ -8,7 +8,7 @@ import ( type Assignment struct { ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` KeyID string `bson:"key_id" json:"key_id"` ServerID string `bson:"server_id" json:"server_id"` AssignedAt time.Time `bson:"assigned_at" json:"assigned_at"` diff --git a/server/internal/models/audit.go b/server/internal/models/audit.go index 61c55fe..6c77456 100644 --- a/server/internal/models/audit.go +++ b/server/internal/models/audit.go @@ -7,12 +7,12 @@ import ( ) type AuditEvent struct { - ID bson.ObjectID `bson:"_id,omitempty" json:"id"` - OrgID string `bson:"org_id" json:"org_id"` - EventType string `bson:"event_type" json:"event_type"` - Actor string `bson:"actor" json:"actor"` - ServerID string `bson:"server_id,omitempty" json:"server_id,omitempty"` - KeyID string `bson:"key_id,omitempty" json:"key_id,omitempty"` - Details string `bson:"details" json:"details"` - CreatedAt time.Time `bson:"created_at" json:"created_at"` + ID bson.ObjectID `bson:"_id,omitempty" json:"id"` + InstanceID string `bson:"instance_id" json:"instance_id"` + EventType string `bson:"event_type" json:"event_type"` + Actor string `bson:"actor" json:"actor"` + ServerID string `bson:"server_id,omitempty" json:"server_id,omitempty"` + KeyID string `bson:"key_id,omitempty" json:"key_id,omitempty"` + Details string `bson:"details" json:"details"` + CreatedAt time.Time `bson:"created_at" json:"created_at"` } diff --git a/server/internal/models/channel.go b/server/internal/models/channel.go index 839a633..10a93db 100644 --- a/server/internal/models/channel.go +++ b/server/internal/models/channel.go @@ -6,7 +6,6 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" ) - const ( ChannelWebhook = "webhook" ChannelSMTP = "smtp" @@ -15,16 +14,13 @@ const ( ChannelTelegram = "telegram" ) - - - type NotificationChannel struct { - ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` - OrgID string `bson:"org_id" json:"org_id"` - ChannelID string `bson:"channel_id" json:"channel_id"` - Name string `bson:"name" json:"name"` - Type string `bson:"type" json:"type"` - Config map[string]string `bson:"config" json:"config"` - Enabled bool `bson:"enabled" json:"enabled"` - CreatedAt time.Time `bson:"created_at" json:"created_at"` + ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` + InstanceID string `bson:"instance_id" json:"instance_id"` + ChannelID string `bson:"channel_id" json:"channel_id"` + Name string `bson:"name" json:"name"` + Type string `bson:"type" json:"type"` + Config map[string]string `bson:"config" json:"config"` + Enabled bool `bson:"enabled" json:"enabled"` + CreatedAt time.Time `bson:"created_at" json:"created_at"` } diff --git a/server/internal/models/console_session.go b/server/internal/models/console_session.go index 3eda4ee..11d76b7 100644 --- a/server/internal/models/console_session.go +++ b/server/internal/models/console_session.go @@ -7,19 +7,17 @@ import ( ) type ConsoleSession struct { - ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` - OrgID string `bson:"org_id" json:"org_id"` - SessionID string `bson:"session_id" json:"session_id"` - ServerID string `bson:"server_id" json:"server_id"` - Protocol string `bson:"protocol" json:"protocol"` - KeyID string `bson:"key_id,omitempty" json:"key_id,omitempty"` - User string `bson:"user" json:"user"` - StartedAt time.Time `bson:"started_at" json:"started_at"` - EndedAt *time.Time `bson:"ended_at,omitempty" json:"ended_at,omitempty"` - ClientIP string `bson:"client_ip,omitempty" json:"client_ip,omitempty"` + ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` + InstanceID string `bson:"instance_id" json:"instance_id"` + SessionID string `bson:"session_id" json:"session_id"` + ServerID string `bson:"server_id" json:"server_id"` + Protocol string `bson:"protocol" json:"protocol"` + KeyID string `bson:"key_id,omitempty" json:"key_id,omitempty"` + User string `bson:"user" json:"user"` + StartedAt time.Time `bson:"started_at" json:"started_at"` + EndedAt *time.Time `bson:"ended_at,omitempty" json:"ended_at,omitempty"` + ClientIP string `bson:"client_ip,omitempty" json:"client_ip,omitempty"` - - TokenConsumedAt *time.Time `bson:"token_consumed_at,omitempty" json:"-"` SSHUsername string `bson:"ssh_username,omitempty" json:"ssh_username,omitempty"` diff --git a/server/internal/models/instance.go b/server/internal/models/instance.go new file mode 100644 index 0000000..b473d7c --- /dev/null +++ b/server/internal/models/instance.go @@ -0,0 +1,7 @@ +package models + +import shared "github.com/mrhid6/vantage/shared/models" + +// Instance is defined in the shared module because sitesvc and the admin +// control plane write the same documents. +type Instance = shared.Instance diff --git a/server/internal/models/key.go b/server/internal/models/key.go index 86ad450..68faef6 100644 --- a/server/internal/models/key.go +++ b/server/internal/models/key.go @@ -8,12 +8,12 @@ import ( type Key struct { ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` KeyID string `bson:"key_id" json:"key_id"` Label string `bson:"label" json:"label"` PublicKey string `bson:"public_key" json:"public_key"` Fingerprint string `bson:"fingerprint" json:"fingerprint"` - Source string `bson:"source" json:"source"` + Source string `bson:"source" json:"source"` GeneratedByServerID string `bson:"generated_by_server_id,omitempty" json:"generated_by_server_id,omitempty"` PrivateKeyEncrypted string `bson:"private_key_enc,omitempty" json:"-"` HasPrivateKey bool `bson:"-" json:"has_private_key"` diff --git a/server/internal/models/monitor.go b/server/internal/models/monitor.go index 8f7860b..940bad2 100644 --- a/server/internal/models/monitor.go +++ b/server/internal/models/monitor.go @@ -6,7 +6,6 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" ) - const ( MonitorHTTP = "http" MonitorTCP = "tcp" @@ -14,15 +13,12 @@ const ( MonitorTLS = "tls" ) - const ( StatusUp = "up" StatusDown = "down" StatusPending = "pending" ) - - const RunnerServer = "server" type MonitorTarget struct { @@ -33,29 +29,29 @@ type MonitorTarget struct { ExpectedStatus int `bson:"expected_status,omitempty" json:"expected_status,omitempty"` Keyword string `bson:"keyword,omitempty" json:"keyword,omitempty"` TLSWarnDays int `bson:"tls_warn_days,omitempty" json:"tls_warn_days,omitempty"` - Insecure bool `bson:"insecure,omitempty" json:"insecure,omitempty"` + Insecure bool `bson:"insecure,omitempty" json:"insecure,omitempty"` } type MonitorState struct { - Status string `bson:"status" json:"status"` + Status string `bson:"status" json:"status"` LastCheckAt *time.Time `bson:"last_check_at,omitempty" json:"last_check_at,omitempty"` LatencyMs int `bson:"latency_ms" json:"latency_ms"` Message string `bson:"message,omitempty" json:"message,omitempty"` CertExpiryAt *time.Time `bson:"cert_expiry_at,omitempty" json:"cert_expiry_at,omitempty"` - Fails int `bson:"fails" json:"fails"` + Fails int `bson:"fails" json:"fails"` LastNotifiedAt *time.Time `bson:"last_notified_at,omitempty" json:"last_notified_at,omitempty"` } type Monitor struct { ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` MonitorID string `bson:"monitor_id" json:"monitor_id"` Name string `bson:"name" json:"name"` - Type string `bson:"type" json:"type"` + Type string `bson:"type" json:"type"` Target MonitorTarget `bson:"target" json:"target"` IntervalSec int `bson:"interval_sec" json:"interval_sec"` - Runner string `bson:"runner" json:"runner"` - Retries int `bson:"retries" json:"retries"` + Runner string `bson:"runner" json:"runner"` + Retries int `bson:"retries" json:"retries"` Enabled bool `bson:"enabled" json:"enabled"` ChannelIDs []string `bson:"channel_ids,omitempty" json:"channel_ids,omitempty"` State MonitorState `bson:"state" json:"state"` @@ -63,7 +59,7 @@ type Monitor struct { } type Incident struct { - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` IncidentID string `bson:"incident_id" json:"incident_id"` MonitorID string `bson:"monitor_id" json:"monitor_id"` StartedAt time.Time `bson:"started_at" json:"started_at"` @@ -72,9 +68,9 @@ type Incident struct { } type Rollup struct { - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` MonitorID string `bson:"monitor_id" json:"monitor_id"` - PeriodStart time.Time `bson:"period_start" json:"period_start"` + PeriodStart time.Time `bson:"period_start" json:"period_start"` Checks int `bson:"checks" json:"checks"` UpCount int `bson:"up_count" json:"up_count"` SumLatency int64 `bson:"sum_latency" json:"sum_latency"` diff --git a/server/internal/models/org.go b/server/internal/models/org.go deleted file mode 100644 index b22816f..0000000 --- a/server/internal/models/org.go +++ /dev/null @@ -1,8 +0,0 @@ -package models - -import shared "github.com/mrhid6/vantage/shared/models" - -// Org is defined in the shared module because sitesvc writes the same -// documents. Aliased rather than re-declared so existing call sites are -// unchanged and the two services cannot drift. -type Org = shared.Org diff --git a/server/internal/models/org_oidc.go b/server/internal/models/org_oidc.go index 2f1d095..b6bead8 100644 --- a/server/internal/models/org_oidc.go +++ b/server/internal/models/org_oidc.go @@ -8,7 +8,7 @@ import ( type OrgOIDC struct { ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` Issuer string `bson:"issuer" json:"issuer"` ClientID string `bson:"client_id" json:"client_id"` ClientSecretEnc string `bson:"client_secret_enc,omitempty" json:"-"` diff --git a/server/internal/models/secret.go b/server/internal/models/secret.go index bec0903..b0a94b0 100644 --- a/server/internal/models/secret.go +++ b/server/internal/models/secret.go @@ -6,18 +6,15 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" ) - - type Secret struct { ID bson.ObjectID `bson:"_id,omitempty" json:"-"` - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` Group string `bson:"group" json:"group"` Key string `bson:"key" json:"key"` EncryptedValue string `bson:"encrypted_value" json:"-"` UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } - type GroupSummary struct { Group string `json:"group"` KeyCount int `json:"key_count"` diff --git a/server/internal/models/server.go b/server/internal/models/server.go index fe685bb..4fc2feb 100644 --- a/server/internal/models/server.go +++ b/server/internal/models/server.go @@ -45,7 +45,7 @@ type Inventory struct { type Server struct { ID bson.ObjectID `bson:"_id,omitempty" json:"_id,omitempty"` - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` ServerID string `bson:"server_id" json:"server_id"` Hostname string `bson:"hostname" json:"hostname"` IPAddress string `bson:"ip_address" json:"ip_address"` diff --git a/server/internal/models/workflow.go b/server/internal/models/workflow.go index 759588c..a7ad86b 100644 --- a/server/internal/models/workflow.go +++ b/server/internal/models/workflow.go @@ -14,16 +14,16 @@ type InputParam struct { type WorkflowStep struct { ID bson.ObjectID `bson:"_id,omitempty" json:"-"` - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` StepID string `bson:"step_id" json:"step_id"` Name string `bson:"name" json:"name"` Description string `bson:"description" json:"description"` - Interpreter string `bson:"interpreter" json:"interpreter"` + Interpreter string `bson:"interpreter" json:"interpreter"` Script string `bson:"script" json:"script"` DeclaredOutputs []string `bson:"declared_outputs" json:"declared_outputs"` DeclaredInputs []InputParam `bson:"declared_inputs" json:"declared_inputs"` SecretRefs []string `bson:"secret_refs" json:"secret_refs"` - Source string `bson:"source" json:"source"` + Source string `bson:"source" json:"source"` Slug string `bson:"slug,omitempty" json:"slug,omitempty"` CreatedAt time.Time `bson:"created_at" json:"created_at"` UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` @@ -33,7 +33,7 @@ type WorkflowStepRef struct { StepID string `bson:"step_id,omitempty" json:"step_id,omitempty"` Inline *WorkflowStep `bson:"inline,omitempty" json:"inline,omitempty"` Order int `bson:"order" json:"order"` - OnFailure string `bson:"on_failure" json:"on_failure"` + OnFailure string `bson:"on_failure" json:"on_failure"` MaxRetries int `bson:"max_retries" json:"max_retries"` Overrides *StepOverride `bson:"overrides,omitempty" json:"overrides,omitempty"` Inputs map[string]string `bson:"inputs,omitempty" json:"inputs,omitempty"` @@ -46,7 +46,7 @@ type StepOverride struct { type Workflow struct { ID bson.ObjectID `bson:"_id,omitempty" json:"-"` - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` WorkflowID string `bson:"workflow_id" json:"workflow_id"` Name string `bson:"name" json:"name"` TargetServerIDs []string `bson:"target_server_ids" json:"target_server_ids"` @@ -55,7 +55,6 @@ type Workflow struct { UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` } - type ResolvedStep struct { Order int `bson:"order" json:"order"` Name string `bson:"name" json:"name"` @@ -70,7 +69,7 @@ type ResolvedStep struct { type StepRun struct { Order int `bson:"order" json:"order"` Name string `bson:"name" json:"name"` - Status string `bson:"status" json:"status"` + Status string `bson:"status" json:"status"` Attempts int `bson:"attempts" json:"attempts"` ExitCode int `bson:"exit_code" json:"exit_code"` LogOffset int64 `bson:"log_offset" json:"log_offset"` @@ -82,7 +81,7 @@ type StepRun struct { type ServerRun struct { ServerID string `bson:"server_id" json:"server_id"` Hostname string `bson:"hostname" json:"hostname"` - Status string `bson:"status" json:"status"` + Status string `bson:"status" json:"status"` StartedAt *time.Time `bson:"started_at,omitempty" json:"started_at,omitempty"` FinishedAt *time.Time `bson:"finished_at,omitempty" json:"finished_at,omitempty"` RunEnv map[string]string `bson:"run_env" json:"run_env"` @@ -91,12 +90,12 @@ type ServerRun struct { type WorkflowRun struct { ID bson.ObjectID `bson:"_id,omitempty" json:"-"` - OrgID string `bson:"org_id" json:"org_id"` + InstanceID string `bson:"instance_id" json:"instance_id"` RunID string `bson:"run_id" json:"run_id"` WorkflowID string `bson:"workflow_id" json:"workflow_id"` Name string `bson:"name" json:"name"` Steps []ResolvedStep `bson:"steps_snapshot" json:"steps_snapshot"` - Status string `bson:"status" json:"status"` + Status string `bson:"status" json:"status"` TriggeredBy string `bson:"triggered_by" json:"triggered_by"` StartedAt time.Time `bson:"started_at" json:"started_at"` FinishedAt *time.Time `bson:"finished_at,omitempty" json:"finished_at,omitempty"` diff --git a/server/internal/monitorsched/scheduler.go b/server/internal/monitorsched/scheduler.go index 5b90837..1d7f11c 100644 --- a/server/internal/monitorsched/scheduler.go +++ b/server/internal/monitorsched/scheduler.go @@ -40,7 +40,7 @@ func loop(ctx context.Context) { mu.Lock() defer mu.Unlock() - + for id, r := range active { m, ok := want[id] if !ok || m.IntervalSec != r.intervalSec { @@ -48,7 +48,7 @@ func loop(ctx context.Context) { delete(active, id) } } - + for id, m := range want { if _, ok := active[id]; ok { continue @@ -86,7 +86,7 @@ func runMonitor(ctx context.Context, m models.Monitor) { } } - run() + run() t := time.NewTicker(interval) defer t.Stop() for { diff --git a/server/internal/notify/dispatch.go b/server/internal/notify/dispatch.go index 00e9545..b4d7fd1 100644 --- a/server/internal/notify/dispatch.go +++ b/server/internal/notify/dispatch.go @@ -1,6 +1,3 @@ - - - package notify import ( @@ -10,7 +7,6 @@ import ( "github.com/mrhid6/vantage/server/internal/models" ) - type Event struct { MonitorName string Type string @@ -20,7 +16,6 @@ type Event struct { Time time.Time } - func (e Event) title() string { verb := "recovered" if e.NewStatus == models.StatusDown { @@ -33,7 +28,6 @@ func (e Event) title() string { return s } - func Dispatch(ch models.NotificationChannel, ev Event) error { switch ch.Type { case models.ChannelWebhook: @@ -51,7 +45,6 @@ func Dispatch(ch models.NotificationChannel, ev Event) error { } } - func Test(ch models.NotificationChannel) error { return Dispatch(ch, Event{ MonitorName: "Test monitor", diff --git a/server/internal/notify/http.go b/server/internal/notify/http.go index 703223b..fa06b7c 100644 --- a/server/internal/notify/http.go +++ b/server/internal/notify/http.go @@ -28,7 +28,6 @@ func postJSON(target string, payload any) error { return nil } - func dispatchWebhook(ch models.NotificationChannel, ev Event) error { target := ch.Config["url"] if target == "" { diff --git a/server/internal/notify/smtp.go b/server/internal/notify/smtp.go index 3537716..dd4e058 100644 --- a/server/internal/notify/smtp.go +++ b/server/internal/notify/smtp.go @@ -13,13 +13,6 @@ import ( const smtpTimeout = 15 * time.Second - - - - - - - func dispatchSMTP(ch models.NotificationChannel, ev Event) error { host := ch.Config["host"] port := ch.Config["port"] @@ -36,7 +29,6 @@ func dispatchSMTP(ch models.NotificationChannel, ev Event) error { } _ = conn.SetDeadline(time.Now().Add(smtpTimeout)) - if port == "465" { conn = tls.Client(conn, &tls.Config{ServerName: host}) } diff --git a/server/internal/notify/template.go b/server/internal/notify/template.go index 49031cf..756383a 100644 --- a/server/internal/notify/template.go +++ b/server/internal/notify/template.go @@ -10,7 +10,6 @@ import ( "github.com/mrhid6/vantage/server/internal/models" ) - const ( colBg = "#0f1117" colSurface = "#1a1d27" @@ -23,7 +22,6 @@ const ( colDanger = "#ef4444" ) - func statusColor(status string) string { switch status { case models.StatusUp: @@ -35,8 +33,6 @@ func statusColor(status string) string { } } - - func buildMIME(from, to, subject, text, htmlBody string) ([]byte, error) { var buf strings.Builder w := multipart.NewWriter(&buf) @@ -150,7 +146,6 @@ func htmlEmail(ev Event) string { ) } - func textEmail(ev Event) string { return strings.Join([]string{ ev.title(), diff --git a/server/internal/services/audit.go b/server/internal/services/audit.go index 82ac79b..9f0a9d9 100644 --- a/server/internal/services/audit.go +++ b/server/internal/services/audit.go @@ -11,25 +11,25 @@ import ( "go.mongodb.org/mongo-driver/v2/mongo/options" ) -func LogEvent(orgID, eventType, actor, serverID, keyID, details string) { +func LogEvent(instanceID, 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, - KeyID: keyID, - Details: details, - CreatedAt: time.Now(), + InstanceID: instanceID, + EventType: eventType, + Actor: actor, + ServerID: serverID, + KeyID: keyID, + Details: details, + CreatedAt: time.Now(), } if _, err := db.Col("audit_logs").InsertOne(ctx, event); err != nil { log.Printf("audit log error: %v", err) } } -func ListAuditEvents(orgID string, limit int64) ([]models.AuditEvent, error) { +func ListAuditEvents(instanceID string, limit int64) ([]models.AuditEvent, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -37,7 +37,7 @@ func ListAuditEvents(orgID string, 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{"org_id": orgID}, opts) + cursor, err := db.Col("audit_logs").Find(ctx, bson.M{"instance_id": instanceID}, opts) if err != nil { return nil, err } diff --git a/server/internal/services/channels.go b/server/internal/services/channels.go index 70d5efc..b79fd68 100644 --- a/server/internal/services/channels.go +++ b/server/internal/services/channels.go @@ -13,10 +13,10 @@ import ( "go.mongodb.org/mongo-driver/v2/mongo/options" ) -func ListChannels(orgID string) ([]models.NotificationChannel, error) { +func ListChannels(instanceID string) ([]models.NotificationChannel, error) { ctx, cancel := monCtx() defer cancel() - cur, err := db.Col("notification_channels").Find(ctx, bson.M{"org_id": orgID}, options.Find().SetSort(bson.M{"created_at": 1})) + cur, err := db.Col("notification_channels").Find(ctx, bson.M{"instance_id": instanceID}, options.Find().SetSort(bson.M{"created_at": 1})) if err != nil { return nil, err } @@ -27,11 +27,11 @@ func ListChannels(orgID string) ([]models.NotificationChannel, error) { return out, nil } -func GetChannel(orgID, channelID string) (*models.NotificationChannel, error) { +func GetChannel(instanceID, 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, "org_id": orgID}).Decode(&ch) + err := db.Col("notification_channels").FindOne(ctx, bson.M{"channel_id": channelID, "instance_id": instanceID}).Decode(&ch) if errors.Is(err, mongo.ErrNoDocuments) { return nil, nil } @@ -41,14 +41,13 @@ func GetChannel(orgID, channelID string) (*models.NotificationChannel, error) { return &ch, nil } - -func GetChannels(orgID string, channelIDs []string) ([]models.NotificationChannel, error) { +func GetChannels(instanceID string, channelIDs []string) ([]models.NotificationChannel, error) { if len(channelIDs) == 0 { return nil, nil } ctx, cancel := monCtx() defer cancel() - cur, err := db.Col("notification_channels").Find(ctx, bson.M{"org_id": orgID, "channel_id": bson.M{"$in": channelIDs}}) + cur, err := db.Col("notification_channels").Find(ctx, bson.M{"instance_id": instanceID, "channel_id": bson.M{"$in": channelIDs}}) if err != nil { return nil, err } @@ -59,11 +58,9 @@ func GetChannels(orgID string, channelIDs []string) ([]models.NotificationChanne return out, nil } - - -func validateChannelIDs(orgID string, channelIDs []string) error { +func validateChannelIDs(instanceID string, channelIDs []string) error { for _, id := range channelIDs { - ch, err := GetChannel(orgID, id) + ch, err := GetChannel(instanceID, id) if err != nil { return err } @@ -74,10 +71,10 @@ func validateChannelIDs(orgID string, channelIDs []string) error { return nil } -func CreateChannel(orgID string, ch *models.NotificationChannel) (*models.NotificationChannel, error) { +func CreateChannel(instanceID string, ch *models.NotificationChannel) (*models.NotificationChannel, error) { ctx, cancel := monCtx() defer cancel() - ch.OrgID = orgID + ch.InstanceID = instanceID ch.ChannelID = uuid.NewString() ch.CreatedAt = time.Now() if ch.Config == nil { @@ -89,23 +86,22 @@ func CreateChannel(orgID string, ch *models.NotificationChannel) (*models.Notifi return ch, nil } -func UpdateChannel(orgID, channelID string, upd bson.M) error { +func UpdateChannel(instanceID, channelID string, upd bson.M) error { ctx, cancel := monCtx() defer cancel() - _, err := db.Col("notification_channels").UpdateOne(ctx, bson.M{"channel_id": channelID, "org_id": orgID}, bson.M{"$set": upd}) + _, err := db.Col("notification_channels").UpdateOne(ctx, bson.M{"channel_id": channelID, "instance_id": instanceID}, bson.M{"$set": upd}) return err } -func DeleteChannel(orgID, channelID string) error { +func DeleteChannel(instanceID, channelID string) error { ctx, cancel := monCtx() defer cancel() - _, err := db.Col("notification_channels").DeleteOne(ctx, bson.M{"channel_id": channelID, "org_id": orgID}) + _, err := db.Col("notification_channels").DeleteOne(ctx, bson.M{"channel_id": channelID, "instance_id": instanceID}) return err } - -func TestChannel(orgID, channelID string) error { - ch, err := GetChannel(orgID, channelID) +func TestChannel(instanceID, channelID string) error { + ch, err := GetChannel(instanceID, channelID) if err != nil { return err } diff --git a/server/internal/services/console.go b/server/internal/services/console.go index 6c6a0b3..4bbbb01 100644 --- a/server/internal/services/console.go +++ b/server/internal/services/console.go @@ -17,7 +17,7 @@ import ( ) func sessionHMACKey() ([]byte, error) { - + k, err := encryptionKey() if err != nil { return nil, err @@ -29,7 +29,6 @@ func sessionHMACKey() ([]byte, error) { func b64(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) } - func SignSessionToken(sessionID string, ttl time.Duration) (string, error) { key, err := sessionHMACKey() if err != nil { @@ -42,7 +41,6 @@ func SignSessionToken(sessionID string, ttl time.Duration) (string, error) { return payload + "." + b64(mac.Sum(nil)), nil } - func VerifySessionToken(token string) (string, error) { parts := strings.Split(token, ".") if len(parts) != 3 { @@ -86,10 +84,6 @@ func portOr(v, def int) string { return strconv.Itoa(v) } - - - - func BuildGuacParams(srv *models.Server, protocol, sshUser, privateKey, passphrase, rdpUser, rdpPass string) (*GuacParams, error) { host := srv.IPAddress switch protocol { @@ -129,19 +123,19 @@ func BuildGuacParams(srv *models.Server, protocol, sshUser, privateKey, passphra } } -func CreateConsoleSession(orgID, serverID, protocol, keyID, user, clientIP string) (*models.ConsoleSession, error) { +func CreateConsoleSession(instanceID, serverID, protocol, keyID, user, clientIP string) (*models.ConsoleSession, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() s := &models.ConsoleSession{ - OrgID: orgID, - SessionID: uuid.NewString(), - ServerID: serverID, - Protocol: protocol, - KeyID: keyID, - User: user, - ClientIP: clientIP, - StartedAt: time.Now(), + InstanceID: instanceID, + SessionID: uuid.NewString(), + ServerID: serverID, + Protocol: protocol, + KeyID: keyID, + User: user, + ClientIP: clientIP, + StartedAt: time.Now(), } if _, err := db.Col("console_sessions").InsertOne(ctx, s); err != nil { return nil, err @@ -149,19 +143,17 @@ func CreateConsoleSession(orgID, serverID, protocol, keyID, user, clientIP strin return s, nil } -func GetConsoleSession(orgID, sessionID string) (*models.ConsoleSession, error) { +func GetConsoleSession(instanceID, sessionID string) (*models.ConsoleSession, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var s models.ConsoleSession - if err := db.Col("console_sessions").FindOne(ctx, bson.M{"session_id": sessionID, "org_id": orgID}).Decode(&s); err != nil { + if err := db.Col("console_sessions").FindOne(ctx, bson.M{"session_id": sessionID, "instance_id": instanceID}).Decode(&s); err != nil { return nil, err } return &s, nil } - - -func StashConsoleRDPCreds(orgID, sessionID, username, password string) error { +func StashConsoleRDPCreds(instanceID, sessionID, username, password string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() u, err := encryptString(username) @@ -173,17 +165,14 @@ func StashConsoleRDPCreds(orgID, sessionID, username, password string) error { return err } _, err = db.Col("console_sessions").UpdateOne(ctx, - bson.M{"session_id": sessionID, "org_id": orgID}, + bson.M{"session_id": sessionID, "instance_id": instanceID}, bson.M{"$set": bson.M{"rdp_user_enc": u, "rdp_pass_enc": p}}, ) return err } - - - -func ConsumeConsoleRDPCreds(orgID, sessionID string) (username, password string, err error) { - s, err := GetConsoleSession(orgID, sessionID) +func ConsumeConsoleRDPCreds(instanceID, sessionID string) (username, password string, err error) { + s, err := GetConsoleSession(instanceID, sessionID) if err != nil { return "", "", err } @@ -203,31 +192,27 @@ func ConsumeConsoleRDPCreds(orgID, sessionID string) (username, password string, ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, _ = db.Col("console_sessions").UpdateOne(ctx, - bson.M{"session_id": sessionID, "org_id": orgID}, + bson.M{"session_id": sessionID, "instance_id": instanceID}, bson.M{"$unset": bson.M{"rdp_user_enc": "", "rdp_pass_enc": ""}}, ) return username, password, nil } - -func SetConsoleSSHUser(orgID, sessionID, username string) error { +func SetConsoleSSHUser(instanceID, sessionID, username string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err := db.Col("console_sessions").UpdateOne(ctx, - bson.M{"session_id": sessionID, "org_id": orgID}, + bson.M{"session_id": sessionID, "instance_id": instanceID}, bson.M{"$set": bson.M{"ssh_username": username}}) return err } - - - -func ConsumeSessionToken(orgID, sessionID string) error { +func ConsumeSessionToken(instanceID, sessionID string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() now := time.Now() res, err := db.Col("console_sessions").UpdateOne(ctx, - bson.M{"session_id": sessionID, "org_id": orgID, "token_consumed_at": nil}, + bson.M{"session_id": sessionID, "instance_id": instanceID, "token_consumed_at": nil}, bson.M{"$set": bson.M{"token_consumed_at": now}}, ) if err != nil { @@ -239,12 +224,12 @@ func ConsumeSessionToken(orgID, sessionID string) error { return nil } -func EndConsoleSession(orgID, sessionID string) error { +func EndConsoleSession(instanceID, sessionID string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() now := time.Now() _, err := db.Col("console_sessions").UpdateOne(ctx, - bson.M{"session_id": sessionID, "org_id": orgID, "ended_at": nil}, + bson.M{"session_id": sessionID, "instance_id": instanceID, "ended_at": nil}, bson.M{"$set": bson.M{"ended_at": now}}, ) return err diff --git a/server/internal/services/coreindexes.go b/server/internal/services/coreindexes.go new file mode 100644 index 0000000..c538d92 --- /dev/null +++ b/server/internal/services/coreindexes.go @@ -0,0 +1,41 @@ +package services + +import ( + "context" + "time" + + "github.com/mrhid6/vantage/server/internal/db" + "github.com/mrhid6/vantage/shared/indexes" + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" + "go.mongodb.org/mongo-driver/v2/mongo/options" +) + +// EnsureAuthIndexes declares the indexes tenant isolation depends on. +// +// It lives here rather than in migrate.go so that the org-to-instance rename +// could touch it without touching migrations 0001 to 0003, which deliberately +// still speak the pre-rename shape. +// +// It MUST run after MigrateOrgToInstance. Creating the instances.slug index +// first would create an empty instances collection, and migration 0004 refuses +// to rename orgs when instances already exists. +func EnsureAuthIndexes() error { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // users.email and instances.slug are declared in the shared module so the + // control plane and sitesvc cannot disagree about them. + if err := indexes.EnsureCoreIndexes(ctx, db.Database); err != nil { + return err + } + + // instance_oidc is control-plane only, so its index stays here. + if _, err := db.Col("instance_oidc").Indexes().CreateOne(ctx, mongo.IndexModel{ + Keys: bson.D{{Key: "instance_id", Value: 1}}, + Options: options.Index().SetUnique(true), + }); err != nil { + return err + } + return nil +} diff --git a/server/internal/services/crypto.go b/server/internal/services/crypto.go index 12c25ef..64f82e1 100644 --- a/server/internal/services/crypto.go +++ b/server/internal/services/crypto.go @@ -22,8 +22,6 @@ func encryptionKey() ([]byte, error) { return key, nil } - - func encryptString(plaintext string) (string, error) { key, err := encryptionKey() if err != nil { @@ -45,7 +43,6 @@ func encryptString(plaintext string) (string, error) { return hex.EncodeToString(sealed), nil } - func decryptString(ciphertextHex string) (string, error) { key, err := encryptionKey() if err != nil { diff --git a/server/internal/services/defaults.go b/server/internal/services/defaults.go index 305dd4a..ab2aba1 100644 --- a/server/internal/services/defaults.go +++ b/server/internal/services/defaults.go @@ -13,7 +13,6 @@ import ( "go.mongodb.org/mongo-driver/v2/mongo/options" ) - func DefaultStepsDir() string { dir := os.Getenv("VANTAGE_DEFAULT_STEPS_DIR") if dir == "" { @@ -23,9 +22,6 @@ func DefaultStepsDir() string { return dir } - - - func readDefaultStepFiles() ([]models.WorkflowStep, error) { matches, err := filepath.Glob(filepath.Join(DefaultStepsDir(), "*.json")) if err != nil { @@ -51,9 +47,7 @@ func readDefaultStepFiles() ([]models.WorkflowStep, error) { return out, nil } - - -func SeedDefaultSteps(orgID string) (created, updated int, err error) { +func SeedDefaultSteps(instanceID string) (created, updated int, err error) { steps, err := readDefaultStepFiles() if err != nil { return 0, 0, err @@ -62,7 +56,7 @@ func SeedDefaultSteps(orgID string) (created, updated int, err error) { defer cancel() col := db.Col("workflow_steps") for _, s := range steps { - filter := bson.M{"org_id": orgID, "slug": s.Slug, "source": "default"} + filter := bson.M{"instance_id": instanceID, "slug": s.Slug, "source": "default"} set := bson.M{ "name": s.Name, "description": s.Description, @@ -76,11 +70,11 @@ func SeedDefaultSteps(orgID string) (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", - "created_at": time.Now(), + "instance_id": instanceID, + "step_id": uuid.New().String(), + "slug": s.Slug, + "source": "default", + "created_at": time.Now(), }, }, options.UpdateOne().SetUpsert(true)) if uerr != nil { diff --git a/server/internal/services/dispatch.go b/server/internal/services/dispatch.go index 43258e2..be666d1 100644 --- a/server/internal/services/dispatch.go +++ b/server/internal/services/dispatch.go @@ -17,13 +17,10 @@ type commandDispatcher struct { channels map[string]chan *pb.ServerCommand } - - var Dispatcher = &commandDispatcher{ channels: make(map[string]chan *pb.ServerCommand), } - func (d *commandDispatcher) Connect(serverID string) chan *pb.ServerCommand { ch := make(chan *pb.ServerCommand, 16) d.mu.Lock() @@ -32,14 +29,12 @@ func (d *commandDispatcher) Connect(serverID string) chan *pb.ServerCommand { return ch } - func (d *commandDispatcher) Disconnect(serverID string) { d.mu.Lock() delete(d.channels, serverID) d.mu.Unlock() } - func (d *commandDispatcher) IsConnected(serverID string) bool { d.mu.RLock() _, ok := d.channels[serverID] @@ -62,15 +57,10 @@ func (d *commandDispatcher) dispatch(serverID string, cmd *pb.ServerCommand) err } } - - func DispatchRunStep(serverID, commandID string, cmd *pb.RunStepCmd) error { return Dispatcher.dispatch(serverID, &pb.ServerCommand{CommandId: commandID, RunStep: cmd}) } - - - func DispatchCleanupWorkspace(serverID, workspaceID string) { if !Dispatcher.IsConnected(serverID) { return @@ -81,7 +71,6 @@ func DispatchCleanupWorkspace(serverID, workspaceID string) { }) } - type KeyGenParams struct { Label string KeyType string @@ -90,15 +79,13 @@ type KeyGenParams struct { Comment string } - - func GetLatestAgentVersion() (string, error) { giteaHost := os.Getenv("GITEA_HOST") if giteaHost == "" { giteaHost = "gitea.example.com" } url := fmt.Sprintf("https://%s/api/v1/repos/mrhid6/vantage/releases?limit=20", giteaHost) - resp, err := http.Get(url) + resp, err := http.Get(url) if err != nil { return "", fmt.Errorf("fetch releases: %w", err) } @@ -122,8 +109,6 @@ func GetLatestAgentVersion() (string, error) { return "", fmt.Errorf("no agent release found") } - - func DispatchUpdateAgent(serverID string) (string, error) { if !Dispatcher.IsConnected(serverID) { return "", fmt.Errorf("agent is not connected to the command stream") @@ -153,7 +138,6 @@ func DispatchUpdateAgent(serverID string) (string, error) { return version, nil } - func DispatchApplyUpdates(serverID string) error { if !Dispatcher.IsConnected(serverID) { return fmt.Errorf("agent is not connected to the command stream") @@ -165,8 +149,6 @@ func DispatchApplyUpdates(serverID string) error { return Dispatcher.dispatch(serverID, cmd) } - - func DispatchDeleteKey(serverID, label string) { if !Dispatcher.IsConnected(serverID) { return @@ -176,13 +158,11 @@ func DispatchDeleteKey(serverID, label string) { DeleteKey: &pb.DeleteKeyCmd{Label: label}, } if err := Dispatcher.dispatch(serverID, cmd); err != nil { - + _ = err } } - - func DispatchGenerateKey(serverID string, p KeyGenParams) (string, error) { if !Dispatcher.IsConnected(serverID) { return "", fmt.Errorf("agent is not connected to the command stream") diff --git a/server/internal/services/org_oidc.go b/server/internal/services/instance_oidc.go similarity index 61% rename from server/internal/services/org_oidc.go rename to server/internal/services/instance_oidc.go index d5843ec..7c3a358 100644 --- a/server/internal/services/org_oidc.go +++ b/server/internal/services/instance_oidc.go @@ -10,32 +10,30 @@ import ( "go.mongodb.org/mongo-driver/v2/mongo/options" ) -func GetOrgOIDC(orgID string) (*models.OrgOIDC, error) { +func GetInstanceOIDC(instanceID string) (*models.OrgOIDC, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var o models.OrgOIDC - err := db.Col("org_oidc").FindOne(ctx, bson.M{"org_id": orgID}).Decode(&o) + err := db.Col("instance_oidc").FindOne(ctx, bson.M{"instance_id": instanceID}).Decode(&o) if err != nil { return nil, err } return &o, nil } -func GetOrgOIDCSecret(orgID string) (string, error) { - o, err := GetOrgOIDC(orgID) +func GetInstanceOIDCSecret(instanceID string) (string, error) { + o, err := GetInstanceOIDC(instanceID) if err != nil { return "", err } return decryptString(o.ClientSecretEnc) } - - -func SaveOrgOIDC(orgID, issuer, clientID, clientSecret string, enabled bool) error { +func SaveOrgOIDC(instanceID, issuer, clientID, clientSecret string, enabled bool) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() set := bson.M{ - "org_id": orgID, "issuer": issuer, "client_id": clientID, + "instance_id": instanceID, "issuer": issuer, "client_id": clientID, "enabled": enabled, "updated_at": time.Now(), } if clientSecret != "" { @@ -45,8 +43,8 @@ func SaveOrgOIDC(orgID, issuer, clientID, clientSecret string, enabled bool) err } set["client_secret_enc"] = enc } - _, err := db.Col("org_oidc").UpdateOne(ctx, - bson.M{"org_id": orgID}, bson.M{"$set": set}, + _, err := db.Col("instance_oidc").UpdateOne(ctx, + bson.M{"instance_id": instanceID}, bson.M{"$set": set}, options.UpdateOne().SetUpsert(true)) return err } diff --git a/server/internal/services/orgs.go b/server/internal/services/instances.go similarity index 59% rename from server/internal/services/orgs.go rename to server/internal/services/instances.go index da36f0b..c66f3eb 100644 --- a/server/internal/services/orgs.go +++ b/server/internal/services/instances.go @@ -13,69 +13,64 @@ import ( "go.mongodb.org/mongo-driver/v2/mongo" ) -func GetOrg(orgID string) (*models.Org, error) { +func GetInstance(instanceID string) (*models.Instance, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - var o models.Org - err := db.Col("orgs").FindOne(ctx, bson.M{"org_id": orgID}).Decode(&o) + var o models.Instance + err := db.Col("instances").FindOne(ctx, bson.M{"instance_id": instanceID}).Decode(&o) if err != nil { return nil, err } return &o, nil } -func GetOrgBySlug(slug string) (*models.Org, error) { +func GetInstanceBySlug(slug string) (*models.Instance, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - var o models.Org - err := db.Col("orgs").FindOne(ctx, bson.M{"slug": slug}).Decode(&o) + var o models.Instance + err := db.Col("instances").FindOne(ctx, bson.M{"slug": slug}).Decode(&o) if err != nil { return nil, err } return &o, nil } - - -func ListOrgIDs() ([]string, error) { +func ListInstanceIDs() ([]string, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - cursor, err := db.Col("orgs").Find(ctx, bson.M{}) + cursor, err := db.Col("instances").Find(ctx, bson.M{}) if err != nil { return nil, err } defer cursor.Close(ctx) - var orgs []models.Org + var orgs []models.Instance 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) + ids = append(ids, o.InstanceID) } return ids, nil } - - - -func CountOrgs() (int64, error) { +func CountInstances() (int64, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - return db.Col("orgs").CountDocuments(ctx, bson.M{}) + return db.Col("instances").CountDocuments(ctx, bson.M{}) } -func FirstOrg() (*models.Org, error) { +func FirstInstance() (*models.Instance, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - var o models.Org - if err := db.Col("orgs").FindOne(ctx, bson.M{}).Decode(&o); err != nil { + var o models.Instance + if err := db.Col("instances").FindOne(ctx, bson.M{}).Decode(&o); err != nil { return nil, err } return &o, nil } -func AdoptOrg(orgID, name string) (*models.Org, error) { +func AdoptInstance(instanceID, name string) (*models.Instance, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -86,7 +81,7 @@ func AdoptOrg(orgID, name string) (*models.Org, error) { slug = slug[:provision.MaxSlugLength] } if len(slug) >= provision.MinSlugLength && !provision.ReservedSlugs[slug] { - n, err := db.Col("orgs").CountDocuments(ctx, bson.M{"slug": slug, "org_id": bson.M{"$ne": orgID}}) + n, err := db.Col("instances").CountDocuments(ctx, bson.M{"slug": slug, "instance_id": bson.M{"$ne": instanceID}}) if err != nil { return nil, err } @@ -95,33 +90,33 @@ func AdoptOrg(orgID, name string) (*models.Org, error) { } } - if _, err := db.Col("orgs").UpdateOne(ctx, bson.M{"org_id": orgID}, bson.M{"$set": set}); err != nil { + if _, err := db.Col("instances").UpdateOne(ctx, bson.M{"instance_id": instanceID}, bson.M{"$set": set}); err != nil { if mongo.IsDuplicateKeyError(err) { return nil, fmt.Errorf("organization slug already taken") } return nil, err } - return GetOrg(orgID) + return GetInstance(instanceID) } -// CreateOrg creates an organisation and seeds its default workflow steps. +// CreateInstance creates an organisation and seeds its default workflow steps. // // The creation rules live in shared/provision because sitesvc creates // organisations too. Seeding stays here: shared must not know about workflow // steps. -func CreateOrg(name string) (*models.Org, error) { +func CreateInstance(name string) (*models.Instance, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - o, err := provision.CreateOrg(ctx, db.Database, name) + o, err := provision.CreateInstance(ctx, db.Database, name) if err != nil { return nil, err } - if created, updated, err := SeedDefaultSteps(o.OrgID); err != nil { - log.Printf("warning: failed to seed default steps for new org %s: %v", o.OrgID, err) + if created, updated, err := SeedDefaultSteps(o.InstanceID); err != nil { + log.Printf("warning: failed to seed default steps for new org %s: %v", o.InstanceID, err) } else { - log.Printf("default steps seeded for new org %s: %d created, %d updated", o.OrgID, created, updated) + log.Printf("default steps seeded for new org %s: %d created, %d updated", o.InstanceID, created, updated) } return o, nil } diff --git a/server/internal/services/inventory.go b/server/internal/services/inventory.go index 29c9ba9..5609cba 100644 --- a/server/internal/services/inventory.go +++ b/server/internal/services/inventory.go @@ -9,8 +9,6 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" ) - - func StoreInventory(serverID string, r *pb.InventoryReport) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() diff --git a/server/internal/services/keys.go b/server/internal/services/keys.go index d0caeac..37f3a81 100644 --- a/server/internal/services/keys.go +++ b/server/internal/services/keys.go @@ -36,9 +36,9 @@ func setKeyMeta(k *models.Key) { k.HasPassphrase = k.PassphraseEncrypted != "" } -func CreateKey(orgID, label, publicKey, source, generatedByServerID, privateKey, passphrase string) (*models.Key, error) { +func CreateKey(instanceID, label, publicKey, source, generatedByServerID, privateKey, passphrase string) (*models.Key, error) { key := &models.Key{ - OrgID: orgID, + InstanceID: instanceID, KeyID: uuid.NewString(), Label: label, PublicKey: publicKey, @@ -72,12 +72,12 @@ func CreateKey(orgID, label, publicKey, source, generatedByServerID, privateKey, return key, nil } -func GetKey(orgID, keyID string) (*models.Key, error) { +func GetKey(instanceID, 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, "org_id": orgID}).Decode(&key) + err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID, "instance_id": instanceID}).Decode(&key) if err != nil { return nil, err } @@ -85,12 +85,12 @@ func GetKey(orgID, keyID string) (*models.Key, error) { return &key, nil } -func GetPrivateKey(orgID, keyID string) (string, error) { +func GetPrivateKey(instanceID, 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, "org_id": orgID}).Decode(&key); err != nil { + if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID, "instance_id": instanceID}).Decode(&key); err != nil { return "", err } if key.PrivateKeyEncrypted == "" { @@ -118,11 +118,11 @@ type KeyWithCount struct { AssignedCount int `bson:"-" json:"assigned_count"` } -func ListKeys(orgID string) ([]KeyWithCount, error) { +func ListKeys(instanceID string) ([]KeyWithCount, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - cursor, err := db.Col("keys").Find(ctx, bson.M{"org_id": orgID}) + cursor, err := db.Col("keys").Find(ctx, bson.M{"instance_id": instanceID}) if err != nil { return nil, err } @@ -137,28 +137,28 @@ func ListKeys(orgID string) ([]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, + "instance_id": instanceID, + "key_id": k.KeyID, + "revoked_at": nil, }) result = append(result, KeyWithCount{Key: k, AssignedCount: int(count)}) } return result, nil } -func DeleteKey(orgID, keyID string) error { +func DeleteKey(instanceID, 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, "org_id": orgID}).Decode(&key); err != nil { + if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": keyID, "instance_id": instanceID}).Decode(&key); err != nil { return err } - if _, err := db.Col("keys").DeleteOne(ctx, bson.M{"key_id": keyID, "org_id": orgID}); err != nil { + if _, err := db.Col("keys").DeleteOne(ctx, bson.M{"key_id": keyID, "instance_id": instanceID}); err != nil { return err } - if _, err := db.Col("assignments").DeleteMany(ctx, bson.M{"key_id": keyID, "org_id": orgID}); err != nil { + if _, err := db.Col("assignments").DeleteMany(ctx, bson.M{"key_id": keyID, "instance_id": instanceID}); err != nil { return err } @@ -168,31 +168,30 @@ func DeleteKey(orgID, keyID string) error { return nil } -func AssignKey(orgID, keyID, serverID string) (*models.Assignment, error) { +func AssignKey(instanceID, keyID, serverID string) (*models.Assignment, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - if _, err := GetKey(orgID, keyID); err != nil { + if _, err := GetKey(instanceID, keyID); err != nil { return nil, fmt.Errorf("key not found") } - if _, err := GetServer(orgID, serverID); err != nil { + if _, err := GetServer(instanceID, serverID); err != nil { return nil, fmt.Errorf("server not found") } - var existing models.Assignment err := db.Col("assignments").FindOne(ctx, bson.M{ - "org_id": orgID, - "key_id": keyID, - "server_id": serverID, - "revoked_at": nil, + "instance_id": instanceID, + "key_id": keyID, + "server_id": serverID, + "revoked_at": nil, }).Decode(&existing) if err == nil { return &existing, nil } a := &models.Assignment{ - OrgID: orgID, + InstanceID: instanceID, KeyID: keyID, ServerID: serverID, AssignedAt: time.Now(), @@ -204,23 +203,23 @@ func AssignKey(orgID, keyID, serverID string) (*models.Assignment, error) { return a, nil } -func RevokeAssignment(orgID, keyID, serverID string) error { +func RevokeAssignment(instanceID, 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{"org_id": orgID, "key_id": keyID, "server_id": serverID, "revoked_at": nil}, + bson.M{"instance_id": instanceID, "key_id": keyID, "server_id": serverID, "revoked_at": nil}, bson.M{"$set": bson.M{"revoked_at": now}}, ) return err } -func GetAssignmentsForKey(orgID, keyID string) ([]models.Assignment, error) { +func GetAssignmentsForKey(instanceID, 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{"org_id": orgID, "key_id": keyID, "revoked_at": nil}) + cursor, err := db.Col("assignments").Find(ctx, bson.M{"instance_id": instanceID, "key_id": keyID, "revoked_at": nil}) if err != nil { return nil, err } @@ -238,11 +237,11 @@ type AssignmentWithServer struct { Server *models.Server `json:"server,omitempty"` } -func GetAssignmentsWithServers(orgID, keyID string) ([]AssignmentWithServer, error) { +func GetAssignmentsWithServers(instanceID, keyID string) ([]AssignmentWithServer, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - cursor, err := db.Col("assignments").Find(ctx, bson.M{"org_id": orgID, "key_id": keyID}) + cursor, err := db.Col("assignments").Find(ctx, bson.M{"instance_id": instanceID, "key_id": keyID}) if err != nil { return nil, err } @@ -257,7 +256,7 @@ func GetAssignmentsWithServers(orgID, keyID string) ([]AssignmentWithServer, err 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, "org_id": orgID}).Decode(&srv); err == nil { + if err := db.Col("servers").FindOne(ctx, bson.M{"server_id": a.ServerID, "instance_id": instanceID}).Decode(&srv); err == nil { item.Server = &srv } result = append(result, item) @@ -270,11 +269,11 @@ type AssignmentWithKey struct { Key *models.Key `json:"key,omitempty"` } -func GetAssignmentsWithKeysForServer(orgID, serverID string) ([]AssignmentWithKey, error) { +func GetAssignmentsWithKeysForServer(instanceID, serverID string) ([]AssignmentWithKey, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - cursor, err := db.Col("assignments").Find(ctx, bson.M{"org_id": orgID, "server_id": serverID}) + cursor, err := db.Col("assignments").Find(ctx, bson.M{"instance_id": instanceID, "server_id": serverID}) if err != nil { return nil, err } @@ -288,7 +287,7 @@ func GetAssignmentsWithKeysForServer(orgID, serverID string) ([]AssignmentWithKe 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, "org_id": orgID}).Decode(&key); err != nil { + if err := db.Col("keys").FindOne(ctx, bson.M{"key_id": a.KeyID, "instance_id": instanceID}).Decode(&key); err != nil { continue } setKeyMeta(&key) diff --git a/server/internal/services/migrate.go b/server/internal/services/migrate.go index 36fc170..78c0531 100644 --- a/server/internal/services/migrate.go +++ b/server/internal/services/migrate.go @@ -8,47 +8,36 @@ import ( "github.com/google/uuid" "github.com/mrhid6/vantage/server/internal/db" - "github.com/mrhid6/vantage/server/internal/models" - "github.com/mrhid6/vantage/shared/indexes" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" - "go.mongodb.org/mongo-driver/v2/mongo/options" ) -var scopedCollections = []string{ +// legacyOrg is the pre-0004 shape of the orgs collection. +// +// Migrations 0001 to 0003 run BEFORE the org-to-instance rename and must keep +// reading and writing org_id in the orgs collection. They deliberately do not +// use shared/models, which has moved on to Instance and instance_id. +type legacyOrg struct { + OrgID string `bson:"org_id"` + Name string `bson:"name"` + Slug string `bson:"slug"` + CreatedAt time.Time `bson:"created_at"` +} + +var backfillCollections = []string{ "servers", "keys", "assignments", "secrets", "workflows", "workflow_steps", "workflow_runs", "audit_logs", "monitors", "notification_channels", "console_sessions", "incidents", "monitor_rollups", } -func EnsureAuthIndexes() error { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // users.email and orgs.slug are declared in the shared module so the - // control plane and sitesvc cannot disagree about them. - if err := indexes.EnsureCoreIndexes(ctx, db.Database); err != nil { - return err - } - - // org_oidc is control-plane only, so its index stays here. - if _, err := db.Col("org_oidc").Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "org_id", Value: 1}}, - Options: options.Index().SetUnique(true), - }); err != nil { - return err - } - return nil -} - -func defaultBackfillOrg(ctx context.Context) (*models.Org, error) { - var org models.Org +func defaultBackfillOrg(ctx context.Context) (*legacyOrg, error) { + var org legacyOrg err := db.Col("orgs").FindOne(ctx, bson.M{"slug": "default"}).Decode(&org) switch { case err == nil: case errors.Is(err, mongo.ErrNoDocuments): - org = models.Org{OrgID: uuid.NewString(), Name: "Default", Slug: "default", CreatedAt: time.Now()} + org = legacyOrg{OrgID: uuid.NewString(), Name: "Default", Slug: "default", CreatedAt: time.Now()} if _, err := db.Col("orgs").InsertOne(ctx, org); err != nil { return nil, err } @@ -67,9 +56,8 @@ func RunMigrations() error { return nil } - needs := false - for _, col := range scopedCollections { + for _, col := range backfillCollections { n, _ := db.Col(col).CountDocuments(ctx, bson.M{"org_id": bson.M{"$exists": false}}) if n > 0 { needs = true @@ -82,7 +70,7 @@ func RunMigrations() error { if err != nil { return err } - for _, col := range scopedCollections { + for _, col := range backfillCollections { if _, err := db.Col(col).UpdateMany(ctx, bson.M{"org_id": bson.M{"$exists": false}}, bson.M{"$set": bson.M{"org_id": org.OrgID}}, @@ -105,7 +93,6 @@ func MigrateMissedOrgScopes() error { return nil } - missed := []string{"audit_logs", "notification_channels"} needs := false for _, col := range missed { @@ -186,7 +173,7 @@ func MigrateSettingsOrg() error { n, _ := db.Col("settings").CountDocuments(ctx, bson.M{"org_id": bson.M{"$exists": false}}) if n > 0 { - var org models.Org + var org legacyOrg orgCount, err := db.Col("orgs").CountDocuments(ctx, bson.M{}) if err != nil { return err @@ -197,7 +184,7 @@ func MigrateSettingsOrg() error { return err } case 0: - org = models.Org{OrgID: uuid.NewString(), Name: "Default", Slug: "default", CreatedAt: time.Now()} + org = legacyOrg{OrgID: uuid.NewString(), Name: "Default", Slug: "default", CreatedAt: time.Now()} if _, err := db.Col("orgs").InsertOne(ctx, org); err != nil { return err } diff --git a/server/internal/services/migrate_instance.go b/server/internal/services/migrate_instance.go new file mode 100644 index 0000000..7bbbc31 --- /dev/null +++ b/server/internal/services/migrate_instance.go @@ -0,0 +1,200 @@ +package services + +import ( + "context" + "fmt" + "log" + + "go.mongodb.org/mongo-driver/v2/bson" + "go.mongodb.org/mongo-driver/v2/mongo" +) + +// ScopedCollections lists every collection carrying the tenant key. +// +// Migration 0004 renames org_id to instance_id in each. A collection missing +// from this list keeps the old field name and becomes invisible to every scoped +// query — so this list is load-bearing, not documentation. +// +// AssertNoScopedCollectionMissed checks at boot that nothing outside this list +// holds an org_id. +// +// The migrations collection is deliberately absent: it is not tenant-scoped. +// The two renamed collections appear under their post-rename names, because the +// migration renames the collections before it renames the field. +var ScopedCollections = []string{ + "instances", + "servers", + "keys", + "assignments", + "users", + "instance_oidc", + "settings", + "secrets", + "workflows", + "workflow_steps", + "workflow_runs", + "monitors", + "incidents", + "monitor_rollups", + "notification_channels", + "console_sessions", + "audit_logs", +} + +// collectionRenames maps the two collections whose names change. Ordered so the +// migration is deterministic. +var collectionRenames = []struct{ from, to string }{ + {"orgs", "instances"}, + {"org_oidc", "instance_oidc"}, +} + +// MigrateOrgToInstance renames the tenant key from org_id to instance_id. +// +// It only ever renames documents. It never deletes, drops or unsets one, so a +// bad deploy is recovered by running the inverse rename (cmd/rename-rollback) +// rather than by restoring a backup. +// +// The steps are not atomic across collections — multi-document transactions +// would require a replica set, which self-hosted installs do not guarantee. +// Instead every step is safely repeatable: a collection rename is skipped when +// the source is already gone, and $rename matches nothing on a document that +// has already been renamed. A run that fails partway is fixed by running it +// again. +// +// It must run BEFORE EnsureAuthIndexes. Creating the instances.slug index first +// would create an empty instances collection, and step 1 below refuses to +// rename orgs onto an existing target. +func MigrateOrgToInstance(ctx context.Context, db *mongo.Database) error { + names, err := db.ListCollectionNames(ctx, bson.M{}) + if err != nil { + return fmt.Errorf("list collections: %w", err) + } + exists := map[string]bool{} + for _, n := range names { + exists[n] = true + } + + // Step 1: rename the collections. + for _, r := range collectionRenames { + switch { + case !exists[r.from]: + // Nothing to rename: either already done or never existed. + continue + case exists[r.to]: + return fmt.Errorf("cannot rename %s to %s: both exist; resolve by hand", r.from, r.to) + } + cmd := bson.D{ + {Key: "renameCollection", Value: db.Name() + "." + r.from}, + {Key: "to", Value: db.Name() + "." + r.to}, + } + if err := db.Client().Database("admin").RunCommand(ctx, cmd).Err(); err != nil { + return fmt.Errorf("rename %s to %s: %w", r.from, r.to, err) + } + log.Printf("0004: renamed collection %s to %s", r.from, r.to) + } + + // Step 2: rename the field. + for _, c := range ScopedCollections { + res, err := db.Collection(c).UpdateMany(ctx, + bson.M{"org_id": bson.M{"$exists": true}}, + bson.M{"$rename": bson.M{"org_id": "instance_id"}}, + ) + if err != nil { + return fmt.Errorf("rename org_id in %s: %w", c, err) + } + if res.ModifiedCount > 0 { + log.Printf("0004: %s renamed %d document(s)", c, res.ModifiedCount) + } + } + + // Step 3: verify before anyone records a marker. Any mismatch aborts, and + // the migration is re-run rather than marked done. + for _, c := range ScopedCollections { + total, err := db.Collection(c).CountDocuments(ctx, bson.M{}) + if err != nil { + return fmt.Errorf("count %s: %w", c, err) + } + if total == 0 { + continue + } + + stale, err := db.Collection(c).CountDocuments(ctx, bson.M{"org_id": bson.M{"$exists": true}}) + if err != nil { + return fmt.Errorf("count stale in %s: %w", c, err) + } + if stale != 0 { + return fmt.Errorf("%s still has %d document(s) with org_id; migration incomplete", c, stale) + } + + scoped, err := db.Collection(c).CountDocuments(ctx, bson.M{"instance_id": bson.M{"$exists": true}}) + if err != nil { + return fmt.Errorf("count scoped in %s: %w", c, err) + } + if scoped != total { + return fmt.Errorf("%s has %d document(s) but only %d carry instance_id", c, total, scoped) + } + } + + // Step 4: indexes keyed on the old field name now point at a field that no + // longer exists. Drop them; the boot-time index builders recreate the + // current ones. Dropping an index touches no documents. + for _, c := range ScopedCollections { + cur, err := db.Collection(c).Indexes().List(ctx) + if err != nil { + return fmt.Errorf("list indexes on %s: %w", c, err) + } + var specs []bson.M + if err := cur.All(ctx, &specs); err != nil { + return fmt.Errorf("decode indexes on %s: %w", c, err) + } + for _, s := range specs { + name, _ := s["name"].(string) + if name == "_id_" { + continue + } + keys, ok := s["key"].(bson.M) + if !ok { + continue + } + if _, keyed := keys["org_id"]; !keyed { + continue + } + if err := db.Collection(c).Indexes().DropOne(ctx, name); err != nil { + return fmt.Errorf("drop index %s on %s: %w", name, c, err) + } + log.Printf("0004: dropped stale index %s on %s", name, c) + } + } + + log.Printf("0004: verified %d collection(s)", len(ScopedCollections)) + return nil +} + +// AssertNoScopedCollectionMissed reports any collection holding an org_id that +// ScopedCollections does not know about. A hit means a collection was added +// without being added to the list, and its tenant key was never renamed. +func AssertNoScopedCollectionMissed(ctx context.Context, db *mongo.Database) error { + known := map[string]bool{} + for _, c := range ScopedCollections { + known[c] = true + } + + names, err := db.ListCollectionNames(ctx, bson.M{}) + if err != nil { + return fmt.Errorf("list collections: %w", err) + } + + for _, n := range names { + if known[n] { + continue + } + count, err := db.Collection(n).CountDocuments(ctx, bson.M{"org_id": bson.M{"$exists": true}}) + if err != nil { + return fmt.Errorf("count %s: %w", n, err) + } + if count > 0 { + return fmt.Errorf("collection %q holds %d document(s) with org_id but is not in ScopedCollections", n, count) + } + } + return nil +} diff --git a/server/internal/services/monitors.go b/server/internal/services/monitors.go index 4cbfc5b..e5569b9 100644 --- a/server/internal/services/monitors.go +++ b/server/internal/services/monitors.go @@ -21,7 +21,6 @@ func monCtx() (context.Context, context.CancelFunc) { return context.WithTimeout(context.Background(), 5*time.Second) } - func SpecFor(m *models.Monitor) checker.Spec { return checker.Spec{ Type: m.Type, @@ -37,10 +36,10 @@ func SpecFor(m *models.Monitor) checker.Spec { } } -func ListMonitors(orgID string) ([]models.Monitor, error) { +func ListMonitors(instanceID string) ([]models.Monitor, error) { ctx, cancel := monCtx() defer cancel() - cur, err := db.Col("monitors").Find(ctx, bson.M{"org_id": orgID}, options.Find().SetSort(bson.M{"created_at": 1})) + cur, err := db.Col("monitors").Find(ctx, bson.M{"instance_id": instanceID}, options.Find().SetSort(bson.M{"created_at": 1})) if err != nil { return nil, err } @@ -51,23 +50,23 @@ func ListMonitors(orgID string) ([]models.Monitor, error) { return out, nil } -func ListMonitorsForRunner(orgID, runner string) ([]models.Monitor, error) { - if orgID == "" { +func ListMonitorsForRunner(instanceID, runner string) ([]models.Monitor, error) { + if instanceID == "" { return nil, errors.New("org id required") } - return listMonitorsForRunner(orgID, runner) + return listMonitorsForRunner(instanceID, runner) } func ListServerScheduledMonitors() ([]models.Monitor, error) { return listMonitorsForRunner("", models.RunnerServer) } -func listMonitorsForRunner(orgID, runner string) ([]models.Monitor, error) { +func listMonitorsForRunner(instanceID, runner string) ([]models.Monitor, error) { ctx, cancel := monCtx() defer cancel() filter := bson.M{"runner": runner, "enabled": true} - if orgID != "" { - filter["org_id"] = orgID + if instanceID != "" { + filter["instance_id"] = instanceID } cur, err := db.Col("monitors").Find(ctx, filter) if err != nil { @@ -80,12 +79,11 @@ func listMonitorsForRunner(orgID, runner string) ([]models.Monitor, error) { return out, nil } - -func GetMonitor(orgID, monitorID string) (*models.Monitor, error) { +func GetMonitor(instanceID, 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) + err := db.Col("monitors").FindOne(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID}).Decode(&m) if errors.Is(err, mongo.ErrNoDocuments) { return nil, nil } @@ -109,26 +107,26 @@ func getMonitorByID(monitorID string) (*models.Monitor, error) { return &m, nil } -func validateRunner(orgID, runner string) error { +func validateRunner(instanceID, runner string) error { if runner == "" || runner == models.RunnerServer { return nil } - if _, err := GetServer(orgID, runner); err != nil { + if _, err := GetServer(instanceID, runner); err != nil { return fmt.Errorf("runner server %s not found", runner) } return nil } -func CreateMonitor(orgID string, m *models.Monitor) (*models.Monitor, error) { +func CreateMonitor(instanceID string, m *models.Monitor) (*models.Monitor, error) { ctx, cancel := monCtx() defer cancel() - if err := validateChannelIDs(orgID, m.ChannelIDs); err != nil { + if err := validateChannelIDs(instanceID, m.ChannelIDs); err != nil { return nil, err } - if err := validateRunner(orgID, m.Runner); err != nil { + if err := validateRunner(instanceID, m.Runner); err != nil { return nil, err } - m.OrgID = orgID + m.InstanceID = instanceID m.MonitorID = uuid.NewString() m.CreatedAt = time.Now() if m.IntervalSec <= 0 { @@ -147,17 +145,16 @@ func CreateMonitor(orgID string, m *models.Monitor) (*models.Monitor, error) { return m, nil } -func UpdateMonitor(orgID, monitorID string, upd bson.M) error { +func UpdateMonitor(instanceID, monitorID string, upd bson.M) error { ctx, cancel := monCtx() defer cancel() - - + if raw, present := upd["channel_ids"]; present { ids, ok := raw.([]string) if !ok { return fmt.Errorf("channel_ids must be a string array") } - if err := validateChannelIDs(orgID, ids); err != nil { + if err := validateChannelIDs(instanceID, ids); err != nil { return err } } @@ -166,43 +163,41 @@ func UpdateMonitor(orgID, monitorID string, upd bson.M) error { if !ok { return fmt.Errorf("runner must be a string") } - if err := validateRunner(orgID, runner); err != nil { + if err := validateRunner(instanceID, runner); err != nil { return err } - - - + if runner == "" { upd["runner"] = models.RunnerServer } } - _, err := db.Col("monitors").UpdateOne(ctx, bson.M{"monitor_id": monitorID, "org_id": orgID}, bson.M{"$set": upd}) + _, err := db.Col("monitors").UpdateOne(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID}, bson.M{"$set": upd}) return err } -func DeleteMonitor(orgID, monitorID string) error { +func DeleteMonitor(instanceID, monitorID string) error { ctx, cancel := monCtx() defer cancel() - res, err := db.Col("monitors").DeleteOne(ctx, bson.M{"monitor_id": monitorID, "org_id": orgID}) + res, err := db.Col("monitors").DeleteOne(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID}) if err != nil { return err } - + if res.DeletedCount == 0 { return nil } - db.Col("incidents").DeleteMany(ctx, bson.M{"monitor_id": monitorID, "org_id": orgID}) - db.Col("monitor_rollups").DeleteMany(ctx, bson.M{"monitor_id": monitorID, "org_id": orgID}) + db.Col("incidents").DeleteMany(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID}) + db.Col("monitor_rollups").DeleteMany(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID}) return nil } -func ListIncidents(orgID, monitorID string, limit int64) ([]models.Incident, error) { +func ListIncidents(instanceID, monitorID string, limit int64) ([]models.Incident, error) { ctx, cancel := monCtx() defer cancel() if limit <= 0 { limit = 50 } - cur, err := db.Col("incidents").Find(ctx, bson.M{"monitor_id": monitorID, "org_id": orgID}, + cur, err := db.Col("incidents").Find(ctx, bson.M{"monitor_id": monitorID, "instance_id": instanceID}, options.Find().SetSort(bson.M{"started_at": -1}).SetLimit(limit)) if err != nil { return nil, err @@ -214,12 +209,11 @@ func ListIncidents(orgID, monitorID string, limit int64) ([]models.Incident, err return out, nil } - -func UptimeRollups(orgID, monitorID string, since time.Time) ([]models.Rollup, error) { +func UptimeRollups(instanceID, monitorID string, since time.Time) ([]models.Rollup, error) { ctx, cancel := monCtx() defer cancel() cur, err := db.Col("monitor_rollups").Find(ctx, - bson.M{"monitor_id": monitorID, "org_id": orgID, "period_start": bson.M{"$gte": since}}, + bson.M{"monitor_id": monitorID, "instance_id": instanceID, "period_start": bson.M{"$gte": since}}, options.Find().SetSort(bson.M{"period_start": 1})) if err != nil { return nil, err @@ -231,18 +225,18 @@ func UptimeRollups(orgID, monitorID string, since time.Time) ([]models.Rollup, e return out, nil } -func IngestResult(orgID, runner, monitorID string, res checker.Result) error { - if orgID == "" { +func IngestResult(instanceID, runner, monitorID string, res checker.Result) error { + if instanceID == "" { return errors.New("org id required") } - return ingestResult(orgID, runner, monitorID, res) + return ingestResult(instanceID, runner, monitorID, res) } func IngestServerScheduledResult(monitorID string, res checker.Result) error { return ingestResult("", models.RunnerServer, monitorID, res) } -func ingestResult(orgID, runner, monitorID string, res checker.Result) error { +func ingestResult(instanceID, runner, monitorID string, res checker.Result) error { ctx, cancel := monCtx() defer cancel() @@ -253,7 +247,7 @@ func ingestResult(orgID, runner, monitorID string, res checker.Result) error { if m == nil { return fmt.Errorf("monitor %s not found", monitorID) } - if orgID != "" && m.OrgID != orgID { + if instanceID != "" && m.InstanceID != instanceID { return fmt.Errorf("monitor %s belongs to another org", monitorID) } if m.Runner != runner { @@ -295,28 +289,25 @@ func ingestResult(orgID, runner, monitorID string, res checker.Result) error { return err } - bucket := now.Truncate(time.Hour) up := 0 if res.Up { up = 1 } - - + db.Col("monitor_rollups").UpdateOne(ctx, bson.M{"monitor_id": monitorID, "period_start": bucket}, bson.M{ "$inc": bson.M{"checks": 1, "up_count": up, "sum_latency": int64(res.LatencyMs)}, - "$setOnInsert": bson.M{"org_id": m.OrgID}, + "$setOnInsert": bson.M{"instance_id": m.InstanceID}, }, options.UpdateOne().SetUpsert(true)) - if newStatus != prev { switch newStatus { case models.StatusDown: inc := models.Incident{ - OrgID: m.OrgID, + InstanceID: m.InstanceID, IncidentID: uuid.NewString(), MonitorID: monitorID, StartedAt: now, @@ -327,7 +318,7 @@ func ingestResult(orgID, runner, monitorID string, res checker.Result) error { case models.StatusUp: if prev == models.StatusDown { db.Col("incidents").UpdateOne(ctx, - bson.M{"monitor_id": monitorID, "org_id": m.OrgID, "resolved_at": nil}, + bson.M{"monitor_id": monitorID, "instance_id": m.InstanceID, "resolved_at": nil}, bson.M{"$set": bson.M{"resolved_at": now}}) notifyTransition(m, newStatus, res.Message) } @@ -336,14 +327,11 @@ func ingestResult(orgID, runner, monitorID string, res checker.Result) error { return nil } - - - func notifyTransition(m *models.Monitor, newStatus, message string) { if len(m.ChannelIDs) == 0 { return } - channels, err := GetChannels(m.OrgID, m.ChannelIDs) + channels, err := GetChannels(m.InstanceID, m.ChannelIDs) if err != nil { log.Printf("notify: load channels for %s: %v", m.MonitorID, err) return @@ -366,5 +354,5 @@ func notifyTransition(m *models.Monitor, newStatus, message string) { } }(ch) } - _ = UpdateMonitor(m.OrgID, m.MonitorID, bson.M{"state.last_notified_at": time.Now()}) + _ = UpdateMonitor(m.InstanceID, m.MonitorID, bson.M{"state.last_notified_at": time.Now()}) } diff --git a/server/internal/services/secrets.go b/server/internal/services/secrets.go index 47fbd6d..5735832 100644 --- a/server/internal/services/secrets.go +++ b/server/internal/services/secrets.go @@ -23,7 +23,7 @@ func EnsureSecretIndexes() error { } _, err := db.Col("secrets").Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "org_id", Value: 1}, {Key: "group", Value: 1}, {Key: "key", Value: 1}}, + Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "group", Value: 1}, {Key: "key", Value: 1}}, Options: options.Index().SetUnique(true), }) return err @@ -38,12 +38,12 @@ func isIndexNotFound(err error) bool { return false } -func ListSecretGroups(orgID string) ([]models.GroupSummary, error) { +func ListSecretGroups(instanceID 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: "$match", Value: bson.D{{Key: "instance_id", Value: instanceID}}}}, {{Key: "$group", Value: bson.D{ {Key: "_id", Value: "$group"}, {Key: "key_count", Value: bson.D{{Key: "$sum", Value: 1}}}, @@ -78,11 +78,11 @@ func ListSecretGroups(orgID string) ([]models.GroupSummary, error) { return groups, nil } -func GetSecretGroup(orgID, group string) ([]models.Secret, error) { +func GetSecretGroup(instanceID, 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{"org_id": orgID, "group": group}, + cursor, err := db.Col("secrets").Find(ctx, bson.M{"instance_id": instanceID, "group": group}, options.Find().SetSort(bson.D{{Key: "key", Value: 1}})) if err != nil { return nil, err @@ -96,8 +96,8 @@ func GetSecretGroup(orgID, group string) ([]models.Secret, error) { return docs, nil } -func GetSecretGroupDecrypted(orgID, group string) (map[string]string, error) { - docs, err := GetSecretGroup(orgID, group) +func GetSecretGroupDecrypted(instanceID, group string) (map[string]string, error) { + docs, err := GetSecretGroup(instanceID, group) if err != nil { return nil, err } @@ -112,12 +112,12 @@ func GetSecretGroupDecrypted(orgID, group string) (map[string]string, error) { return result, nil } -func RevealSecret(orgID, group, key string) (string, error) { +func RevealSecret(instanceID, 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{"org_id": orgID, "group": group, "key": key}).Decode(&doc) + err := db.Col("secrets").FindOne(ctx, bson.M{"instance_id": instanceID, "group": group, "key": key}).Decode(&doc) if err == mongo.ErrNoDocuments { return "", fmt.Errorf("secret not found") } @@ -127,7 +127,7 @@ func RevealSecret(orgID, group, key string) (string, error) { return decryptString(doc.EncryptedValue) } -func UpsertSecrets(orgID, group string, values map[string]string) error { +func UpsertSecrets(instanceID, group string, values map[string]string) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -137,9 +137,9 @@ func UpsertSecrets(orgID, group string, values map[string]string) error { return fmt.Errorf("encrypt %s: %w", key, err) } _, err = db.Col("secrets").UpdateOne(ctx, - bson.M{"org_id": orgID, "group": group, "key": key}, + bson.M{"instance_id": instanceID, "group": group, "key": key}, bson.M{"$set": bson.M{ - "org_id": orgID, + "instance_id": instanceID, "encrypted_value": encrypted, "updated_at": time.Now(), }}, @@ -152,7 +152,6 @@ func UpsertSecrets(orgID, group string, values map[string]string) error { return nil } - func SortedKeys(m map[string]string) []string { keys := make([]string, 0, len(m)) for k := range m { @@ -162,20 +161,18 @@ func SortedKeys(m map[string]string) []string { return keys } - -func DeleteSecret(orgID, group, key string) error { +func DeleteSecret(instanceID, group, key string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - _, err := db.Col("secrets").DeleteOne(ctx, bson.M{"org_id": orgID, "group": group, "key": key}) + _, err := db.Col("secrets").DeleteOne(ctx, bson.M{"instance_id": instanceID, "group": group, "key": key}) return err } - -func DeleteSecretGroup(orgID, group string) error { +func DeleteSecretGroup(instanceID, group string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - _, err := db.Col("secrets").DeleteMany(ctx, bson.M{"org_id": orgID, "group": group}) + _, err := db.Col("secrets").DeleteMany(ctx, bson.M{"instance_id": instanceID, "group": group}) return err } diff --git a/server/internal/services/servers.go b/server/internal/services/servers.go index 53f1094..a9d702b 100644 --- a/server/internal/services/servers.go +++ b/server/internal/services/servers.go @@ -30,14 +30,14 @@ func HashToken(token string) string { return hex.EncodeToString(sum[:]) } -func CreateServer(orgID string) (*models.Server, string, error) { +func CreateServer(instanceID 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, + InstanceID: instanceID, ServerID: uuid.NewString(), PreRegToken: token, PreRegExpires: &expires, @@ -54,21 +54,18 @@ func CreateServer(orgID string) (*models.Server, string, error) { return s, token, nil } - -func GetServer(orgID, serverID string) (*models.Server, error) { +func GetServer(instanceID, 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) + err := db.Col("servers").FindOne(ctx, bson.M{"server_id": serverID, "instance_id": instanceID}).Decode(&s) if err != nil { return nil, err } return &s, nil } - - func getServerByID(serverID string) (*models.Server, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -96,9 +93,6 @@ func GetServerByPreRegToken(token string) (*models.Server, error) { return &s, nil } - - - func OSTypeFromInfo(osInfo string) string { if strings.HasPrefix(strings.ToLower(osInfo), "windows") { return "windows" @@ -106,8 +100,6 @@ func OSTypeFromInfo(osInfo string) string { return "linux" } - - func defaultConsoleFields(osType string) (protocols []string, sshPort, rdpPort int) { if osType == "windows" { return []string{"rdp"}, 22, 3389 @@ -181,19 +173,13 @@ func ValidateAgentToken(serverID, agentToken string) (*models.Server, error) { if err != nil { return nil, fmt.Errorf("invalid agent token") } - - - if s.OrgID == "" { + + if s.InstanceID == "" { return nil, fmt.Errorf("server %s has no org", serverID) } return &s, nil } - - - - - func BackfillConsoleConfig(srv *models.Server) error { if srv == nil || len(srv.ConsoleProtocols) > 0 { return nil @@ -236,12 +222,12 @@ func UpdateServerLastSeen(serverID, agentVersion string) error { return err } -func ListServers(orgID string) ([]models.Server, error) { +func ListServers(instanceID 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{"org_id": orgID}, opts) + cursor, err := db.Col("servers").Find(ctx, bson.M{"instance_id": instanceID}, opts) if err != nil { return nil, err } @@ -254,16 +240,16 @@ func ListServers(orgID string) ([]models.Server, error) { return servers, nil } -func DeleteServer(orgID, serverID string) error { +func DeleteServer(instanceID, 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, "org_id": orgID}) + _, err := db.Col("servers").DeleteOne(ctx, bson.M{"server_id": serverID, "instance_id": instanceID}) if err != nil { return err } - - _, err = db.Col("assignments").DeleteMany(ctx, bson.M{"server_id": serverID, "org_id": orgID}) + + _, err = db.Col("assignments").DeleteMany(ctx, bson.M{"server_id": serverID, "instance_id": instanceID}) return err } @@ -283,42 +269,31 @@ func StoreAvailableUpdates(serverID string, pkgs []models.PackageUpdate) error { } func MarkOfflineServers() error { - orgIDs, err := ListOrgIDs() + instanceIDs, err := ListInstanceIDs() if err != nil { return err } - - - - - for _, orgID := range orgIDs { - if err := markOfflineForFilter(bson.M{"org_id": orgID}, orgID); err != nil { - log.Printf("offline sweep failed for org %s: %v", orgID, err) + for _, instanceID := range instanceIDs { + if err := markOfflineForFilter(bson.M{"instance_id": instanceID}, instanceID); err != nil { + log.Printf("offline sweep failed for org %s: %v", instanceID, err) } } - - - - - if err := markOfflineForFilter(bson.M{"org_id": bson.M{"$nin": orgIDs}}, ""); err != nil { + if err := markOfflineForFilter(bson.M{"instance_id": bson.M{"$nin": instanceIDs}}, ""); err != nil { log.Printf("offline sweep failed for orphaned servers: %v", err) } return nil } - - - -func markOfflineForFilter(scope bson.M, orgID string) error { +func markOfflineForFilter(scope bson.M, instanceID string) error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() var settings *models.Settings thresholdMinutes := 5 - if orgID != "" { - settings, _ = GetSettings(orgID) + if instanceID != "" { + settings, _ = GetSettings(instanceID) if settings != nil && settings.Alerts.OfflineThresholdMinutes > 0 { thresholdMinutes = settings.Alerts.OfflineThresholdMinutes } @@ -333,7 +308,6 @@ func markOfflineForFilter(scope bson.M, orgID string) error { filter[k] = v } - cursor, err := db.Col("servers").Find(ctx, filter) if err != nil { return err @@ -349,7 +323,7 @@ func markOfflineForFilter(scope bson.M, orgID string) error { } for _, s := range goingOffline { - LogEvent(s.OrgID, "server.offline", "system", s.ServerID, "", fmt.Sprintf("%s (%s) went offline", s.Hostname, s.IPAddress)) + LogEvent(s.InstanceID, "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) } diff --git a/server/internal/services/settings.go b/server/internal/services/settings.go index fcaae37..2be0661 100644 --- a/server/internal/services/settings.go +++ b/server/internal/services/settings.go @@ -34,9 +34,6 @@ var defaultSettings = models.Settings{ }, } - - - func EnsureSettingsIndexes() error { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -46,16 +43,12 @@ func EnsureSettingsIndexes() error { } if _, err := db.Col("settings").Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "org_id", Value: 1}}, + Keys: bson.D{{Key: "instance_id", Value: 1}}, Options: options.Index().SetUnique(true), }); err != nil { return err } - - - - _, err := db.Col("settings").Indexes().CreateOne(ctx, mongo.IndexModel{ Keys: bson.D{{Key: "secrets.read_token_hash", Value: 1}}, Options: options.Index().SetUnique(true).SetName("settings_read_token_hash_unique"). @@ -66,15 +59,15 @@ func EnsureSettingsIndexes() error { return err } -func GetSettings(orgID string) (*models.Settings, error) { +func GetSettings(instanceID string) (*models.Settings, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var s models.Settings - err := db.Col("settings").FindOne(ctx, bson.M{"org_id": orgID}).Decode(&s) + err := db.Col("settings").FindOne(ctx, bson.M{"instance_id": instanceID}).Decode(&s) if err == mongo.ErrNoDocuments { cp := defaultSettings - cp.OrgID = orgID + cp.InstanceID = instanceID return &cp, nil } if err != nil { @@ -89,9 +82,7 @@ func hashToken(token string) string { return hex.EncodeToString(sum[:]) } - - -func RotateSecretsReadToken(orgID string) (string, error) { +func RotateSecretsReadToken(instanceID string) (string, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -102,13 +93,13 @@ func RotateSecretsReadToken(orgID string) (string, error) { token := hex.EncodeToString(raw) _, err := db.Col("settings").UpdateOne(ctx, - bson.M{"org_id": orgID}, + bson.M{"instance_id": instanceID}, bson.M{ "$set": bson.M{ "secrets.read_token_hash": hashToken(token), "secrets.rotated_at": time.Now(), }, - "$setOnInsert": bson.M{"org_id": orgID}, + "$setOnInsert": bson.M{"instance_id": instanceID}, }, options.UpdateOne().SetUpsert(true), ) @@ -118,9 +109,6 @@ func RotateSecretsReadToken(orgID string) (string, error) { return token, nil } - - - func ResolveSecretsReadToken(token string) (string, bool) { if token == "" { return "", false @@ -130,7 +118,7 @@ func ResolveSecretsReadToken(token string) (string, bool) { var s models.Settings err := db.Col("settings").FindOne(ctx, bson.M{"secrets.read_token_hash": hashToken(token)}).Decode(&s) - if err != nil || s.Secrets.ReadTokenHash == "" || s.OrgID == "" { + if err != nil || s.Secrets.ReadTokenHash == "" || s.InstanceID == "" { return "", false } expected, err := hex.DecodeString(s.Secrets.ReadTokenHash) @@ -141,10 +129,10 @@ func ResolveSecretsReadToken(token string) (string, bool) { if subtle.ConstantTimeCompare(expected, got[:]) != 1 { return "", false } - return s.OrgID, true + return s.InstanceID, true } -func SaveSettings(orgID string, alerts models.AlertSettings, email models.EmailSettings, retentionDays *int) error { +func SaveSettings(instanceID string, alerts models.AlertSettings, email models.EmailSettings, retentionDays *int) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -160,17 +148,15 @@ func SaveSettings(orgID string, alerts models.AlertSettings, email models.EmailS set["workflow_log_retention_days"] = *retentionDays } _, err := db.Col("settings").UpdateOne(ctx, - bson.M{"org_id": orgID}, - bson.M{"$set": set, "$setOnInsert": bson.M{"org_id": orgID}}, + bson.M{"instance_id": instanceID}, + bson.M{"$set": set, "$setOnInsert": bson.M{"instance_id": instanceID}}, options.UpdateOne().SetUpsert(true), ) return err } - - -func GetWorkflowLogRetentionDays(orgID string) (int, error) { - s, err := GetSettings(orgID) +func GetWorkflowLogRetentionDays(instanceID string) (int, error) { + s, err := GetSettings(instanceID) if err != nil { return 30, err } @@ -241,7 +227,6 @@ func SendOfflineEmail(cfg models.EmailSettings, hostname, serverID, ipAddress st } } - func sendMailTLS(addr, host string, auth smtp.Auth, from string, to []string, msg []byte) error { conn, err := tls.Dial("tcp", addr, &tls.Config{ServerName: host}) if err != nil { @@ -278,4 +263,3 @@ func sendMailTLS(addr, host string, auth smtp.Auth, from string, to []string, ms } return c.Quit() } - diff --git a/server/internal/services/stepio.go b/server/internal/services/stepio.go index 6f2e50d..1339ae8 100644 --- a/server/internal/services/stepio.go +++ b/server/internal/services/stepio.go @@ -9,7 +9,6 @@ import ( const StepDocKind = "vantage.step/v1" - type StepDoc struct { Kind string `json:"kind"` Name string `json:"name"` @@ -21,7 +20,6 @@ type StepDoc struct { SecretRefs []string `json:"secret_refs"` } - func ExportStepDoc(s models.WorkflowStep) StepDoc { return StepDoc{ Kind: StepDocKind, @@ -35,8 +33,6 @@ func ExportStepDoc(s models.WorkflowStep) StepDoc { } } - - func ParseStepDoc(b []byte) (models.WorkflowStep, error) { var d StepDoc if err := json.Unmarshal(b, &d); err != nil { @@ -65,20 +61,18 @@ func ParseStepDoc(b []byte) (models.WorkflowStep, error) { }, nil } - -func ImportStepToLibrary(orgID string, b []byte) (*models.WorkflowStep, error) { +func ImportStepToLibrary(instanceID string, b []byte) (*models.WorkflowStep, error) { s, err := ParseStepDoc(b) if err != nil { return nil, err } - return CreateStep(orgID, s) + return CreateStep(instanceID, s) } - -func ExportStep(orgID, stepID string) ([]byte, error) { +func ExportStep(instanceID, stepID string) ([]byte, error) { ctx, cancel := wfCtx() defer cancel() - s, err := getStep(ctx, orgID, stepID) + s, err := getStep(ctx, instanceID, stepID) if err != nil { return nil, err } diff --git a/server/internal/services/steplogs.go b/server/internal/services/steplogs.go index 84b6714..36893e1 100644 --- a/server/internal/services/steplogs.go +++ b/server/internal/services/steplogs.go @@ -14,7 +14,6 @@ import ( "go.mongodb.org/mongo-driver/v2/mongo" ) - func WorkflowLogDir() string { dir := os.Getenv("VANTAGE_WORKFLOW_LOG_DIR") if dir == "" { @@ -24,19 +23,14 @@ func WorkflowLogDir() string { return dir } - func ServerRunLogPath(runID, serverID string) string { return filepath.Join(WorkflowLogDir(), runID, serverID+".log") } - - func logTS() string { return time.Now().UTC().Format("2006-01-02T15:04:05.000") + "Z" } - - func AppendMarker(runID, serverID, text string) (int64, error) { path := ServerRunLogPath(runID, serverID) if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { @@ -47,19 +41,17 @@ func AppendMarker(runID, serverID, text string) (int64, error) { return 0, err } defer f.Close() - off, _ := f.Seek(0, 2) + off, _ := f.Seek(0, 2) if _, err := f.WriteString("[" + logTS() + "] " + text + "\n"); err != nil { return off, err } return off, nil } - - type stepLogWriter struct { mu sync.Mutex f *os.File - carry []byte + carry []byte secrets []string } @@ -70,7 +62,6 @@ type stepLogRegistry struct { var StepLogs = &stepLogRegistry{writers: make(map[string]*stepLogWriter)} - func (r *stepLogRegistry) Open(commandID, path string, secrets []string) error { if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { return err @@ -92,10 +83,6 @@ func (r *stepLogRegistry) get(commandID string) *stepLogWriter { return r.writers[commandID] } - - - - func (r *stepLogRegistry) Append(commandID string, data []byte) { w := r.get(commandID) if w == nil { @@ -115,7 +102,6 @@ func (r *stepLogRegistry) Append(commandID string, data []byte) { w.carry = append([]byte{}, buf...) } - func (w *stepLogWriter) writeLine(line []byte) { masked := maskBytes(line, w.secrets) _, _ = w.f.WriteString("[" + logTS() + "] ") @@ -123,7 +109,6 @@ func (w *stepLogWriter) writeLine(line []byte) { _, _ = w.f.WriteString("\n") } - func (r *stepLogRegistry) Close(commandID string) { r.mu.Lock() w := r.writers[commandID] @@ -152,9 +137,6 @@ func maskBytes(b []byte, secrets []string) []byte { return []byte(s) } - - - func StartLogSweeper() { go func() { sweepLogs() @@ -166,10 +148,6 @@ func StartLogSweeper() { }() } - - - - func sweepLogs() { base := WorkflowLogDir() entries, err := os.ReadDir(base) @@ -186,30 +164,28 @@ func sweepLogs() { runID := e.Name() dir := filepath.Join(base, runID) - orgID, finishedAt, found, err := runRetentionInfo(runID) + instanceID, finishedAt, found, err := runRetentionInfo(runID) if err != nil { - - - + log.Printf("log sweep: retention lookup failed for run %s: %v", runID, err) continue } if found && finishedAt == nil { - continue + continue } - days, ok := cache[orgID] + days, ok := cache[instanceID] if !ok { days = defaultRetentionDays - if orgID != "" { - if v, err := GetWorkflowLogRetentionDays(orgID); err == nil { + if instanceID != "" { + if v, err := GetWorkflowLogRetentionDays(instanceID); err == nil { days = v } } - cache[orgID] = days + cache[instanceID] = days } if days <= 0 { - continue + continue } cutoff := now.AddDate(0, 0, -days) @@ -219,7 +195,7 @@ func sweepLogs() { } continue } - + if fi, e := os.Stat(dir); e == nil && fi.ModTime().Before(cutoff) { _ = os.RemoveAll(dir) } @@ -228,14 +204,11 @@ func sweepLogs() { const defaultRetentionDays = 30 - - - func runRetentionInfo(runID string) (string, *time.Time, bool, error) { ctx, cancel := wfCtx() defer cancel() var run struct { - OrgID string `bson:"org_id"` + InstanceID string `bson:"instance_id"` FinishedAt *time.Time `bson:"finished_at"` } err := db.Col("workflow_runs").FindOne(ctx, bson.M{"run_id": runID}).Decode(&run) @@ -245,5 +218,5 @@ func runRetentionInfo(runID string) (string, *time.Time, bool, error) { if err != nil { return "", nil, false, err } - return run.OrgID, run.FinishedAt, true, nil + return run.InstanceID, run.FinishedAt, true, nil } diff --git a/server/internal/services/stepresults.go b/server/internal/services/stepresults.go index 4b9dd11..464911f 100644 --- a/server/internal/services/stepresults.go +++ b/server/internal/services/stepresults.go @@ -11,12 +11,8 @@ type stepResultRegistry struct { pending map[string]chan *pb.StepResult } - - var StepResults = &stepResultRegistry{pending: make(map[string]chan *pb.StepResult)} - - func (r *stepResultRegistry) Await(commandID string) <-chan *pb.StepResult { ch := make(chan *pb.StepResult, 1) r.mu.Lock() @@ -25,14 +21,12 @@ func (r *stepResultRegistry) Await(commandID string) <-chan *pb.StepResult { return ch } - func (r *stepResultRegistry) Cancel(commandID string) { r.mu.Lock() delete(r.pending, commandID) r.mu.Unlock() } - func (r *stepResultRegistry) Deliver(res *pb.StepResult) { if res == nil { return diff --git a/server/internal/services/stepscan.go b/server/internal/services/stepscan.go index ccc2236..1d87595 100644 --- a/server/internal/services/stepscan.go +++ b/server/internal/services/stepscan.go @@ -5,12 +5,8 @@ import ( "strings" ) - var keyAssign = regexp.MustCompile(`([A-Za-z_][A-Za-z0-9_]*)=`) - - - func DeriveOutputs(script string) []string { out := []string{} seen := map[string]bool{} @@ -20,7 +16,7 @@ func DeriveOutputs(script string) []string { } for _, m := range keyAssign.FindAllStringSubmatch(line, -1) { key := m[1] - + if key == "WORKFLOW_ENV" || key == "env" { continue } @@ -36,4 +32,3 @@ func DeriveOutputs(script string) []string { // Slugify lived here and was mirrored by hand in sitesvc. It now has a single // definition in shared/provision, which both services import. - diff --git a/server/internal/services/sync.go b/server/internal/services/sync.go index 9cc4dd3..0803c83 100644 --- a/server/internal/services/sync.go +++ b/server/internal/services/sync.go @@ -13,17 +13,15 @@ func BuildAuthorizedKeys(serverID string) ([]string, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - - srv, err := getServerByID(serverID) if err != nil { return nil, err } cursor, err := db.Col("assignments").Find(ctx, bson.M{ - "org_id": srv.OrgID, - "server_id": serverID, - "revoked_at": nil, + "instance_id": srv.InstanceID, + "server_id": serverID, + "revoked_at": nil, }) if err != nil { return nil, err @@ -38,7 +36,7 @@ func BuildAuthorizedKeys(serverID string) ([]string, error) { var lines []string for _, a := range assignments { var key models.Key - err := db.Col("keys").FindOne(ctx, bson.M{"key_id": a.KeyID, "org_id": srv.OrgID}).Decode(&key) + err := db.Col("keys").FindOne(ctx, bson.M{"key_id": a.KeyID, "instance_id": srv.InstanceID}).Decode(&key) if err != nil { continue } diff --git a/server/internal/services/users.go b/server/internal/services/users.go index 6f8a5b0..99b7c67 100644 --- a/server/internal/services/users.go +++ b/server/internal/services/users.go @@ -14,53 +14,46 @@ import ( "golang.org/x/crypto/bcrypt" ) - - var ErrLastOwner = errors.New("this is the organization's last owner promote another member to owner first") - - - func CountUsers() (int64, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() return db.Col("users").CountDocuments(ctx, bson.M{}) } -func CountOrgUsers(orgID string) (int64, error) { +func CountInstanceUsers(instanceID string) (int64, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - return db.Col("users").CountDocuments(ctx, bson.M{"org_id": orgID}) + return db.Col("users").CountDocuments(ctx, bson.M{"instance_id": instanceID}) } - - -func countOtherOwners(orgID, exceptUserID string) (int64, error) { +func countOtherOwners(instanceID, exceptUserID string) (int64, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() return db.Col("users").CountDocuments(ctx, bson.M{ - "org_id": orgID, - "role": models.RoleOwner, - "user_id": bson.M{"$ne": exceptUserID}, + "instance_id": instanceID, + "role": models.RoleOwner, + "user_id": bson.M{"$ne": exceptUserID}, }) } -func GetUserInOrg(orgID, userID string) (*models.User, error) { +func GetUserInInstance(instanceID, userID string) (*models.User, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() var u models.User - err := db.Col("users").FindOne(ctx, bson.M{"user_id": userID, "org_id": orgID}).Decode(&u) + err := db.Col("users").FindOne(ctx, bson.M{"user_id": userID, "instance_id": instanceID}).Decode(&u) if err != nil { return nil, err } return &u, nil } -func CreateUser(orgID, email, password, role, authSource string) (*models.User, error) { +func CreateUser(instanceID, email, password, role, authSource string) (*models.User, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - u, err := provision.CreateUser(ctx, db.Database, orgID, email, password, role, authSource) + u, err := provision.CreateUser(ctx, db.Database, instanceID, email, password, role, authSource) if errors.Is(err, provision.ErrEmailTaken) { // Preserve the exact error string the API returned before this call // was delegated to the shared module. @@ -97,10 +90,10 @@ func TouchLastLogin(userID string) error { return err } -func ListUsers(orgID string) ([]models.User, error) { +func ListUsers(instanceID string) ([]models.User, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - cursor, err := db.Col("users").Find(ctx, bson.M{"org_id": orgID}) + cursor, err := db.Col("users").Find(ctx, bson.M{"instance_id": instanceID}) if err != nil { return nil, err } @@ -112,17 +105,17 @@ func ListUsers(orgID string) ([]models.User, error) { return users, nil } -func UpdateUserRole(orgID, userID, role string) error { +func UpdateUserRole(instanceID, userID, role string) error { if !models.ValidRole(role) { return fmt.Errorf("invalid role %q", role) } - target, err := GetUserInOrg(orgID, userID) + target, err := GetUserInInstance(instanceID, userID) if err != nil { return fmt.Errorf("user not found") } - + if target.Role == models.RoleOwner && role != models.RoleOwner { - others, err := countOtherOwners(orgID, userID) + others, err := countOtherOwners(instanceID, userID) if err != nil { return err } @@ -134,18 +127,18 @@ func UpdateUserRole(orgID, userID, role string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _, err = db.Col("users").UpdateOne(ctx, - bson.M{"user_id": userID, "org_id": orgID}, + bson.M{"user_id": userID, "instance_id": instanceID}, bson.M{"$set": bson.M{"role": role}}) return err } -func DeleteUser(orgID, userID string) error { - target, err := GetUserInOrg(orgID, userID) +func DeleteUser(instanceID, userID string) error { + target, err := GetUserInInstance(instanceID, userID) if err != nil { return fmt.Errorf("user not found") } if target.Role == models.RoleOwner { - others, err := countOtherOwners(orgID, userID) + others, err := countOtherOwners(instanceID, userID) if err != nil { return err } @@ -156,6 +149,6 @@ func DeleteUser(orgID, userID string) error { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - _, err = db.Col("users").DeleteOne(ctx, bson.M{"user_id": userID, "org_id": orgID}) + _, err = db.Col("users").DeleteOne(ctx, bson.M{"user_id": userID, "instance_id": instanceID}) return err } diff --git a/server/internal/services/validate.go b/server/internal/services/validate.go index a3c83e3..404ba89 100644 --- a/server/internal/services/validate.go +++ b/server/internal/services/validate.go @@ -6,7 +6,6 @@ import ( "github.com/mrhid6/vantage/server/internal/models" ) - func ValidateWorkflow(w models.Workflow) error { for i, ref := range w.Steps { hasLib := ref.StepID != "" diff --git a/server/internal/services/workflow_runner.go b/server/internal/services/workflow_runner.go index 0da1cfb..8634e21 100644 --- a/server/internal/services/workflow_runner.go +++ b/server/internal/services/workflow_runner.go @@ -17,8 +17,8 @@ import ( const stepDispatchGrace = 15 * time.Second -func TriggerWorkflow(orgID, workflowID, actor string) (string, error) { - wf, err := GetWorkflow(orgID, workflowID) +func TriggerWorkflow(instanceID, workflowID, actor string) (string, error) { + wf, err := GetWorkflow(instanceID, workflowID) if err != nil { return "", err } @@ -29,24 +29,24 @@ func TriggerWorkflow(orgID, workflowID, actor string) (string, error) { return "", fmt.Errorf("workflow has no steps") } - if err := validateTargetServers(orgID, wf.TargetServerIDs); err != nil { + if err := validateTargetServers(instanceID, wf.TargetServerIDs); err != nil { return "", err } ctx, cancel := wfCtx() - running := db.Col("workflow_runs").FindOne(ctx, bson.M{"org_id": orgID, "workflow_id": workflowID, "status": "running"}) + running := db.Col("workflow_runs").FindOne(ctx, bson.M{"instance_id": instanceID, "workflow_id": workflowID, "status": "running"}) cancel() if running.Err() == nil { return "", fmt.Errorf("workflow already has a run in progress") } - resolved, err := resolveSteps(orgID, wf) + resolved, err := resolveSteps(instanceID, wf) if err != nil { return "", err } run := models.WorkflowRun{ - OrgID: orgID, + InstanceID: instanceID, RunID: uuid.New().String(), WorkflowID: workflowID, Name: wf.Name, @@ -78,7 +78,7 @@ func TriggerWorkflow(orgID, workflowID, actor string) (string, error) { return run.RunID, nil } -func resolveSteps(orgID string, wf *models.Workflow) ([]models.ResolvedStep, error) { +func resolveSteps(instanceID string, wf *models.Workflow) ([]models.ResolvedStep, error) { ctx, cancel := wfCtx() defer cancel() out := make([]models.ResolvedStep, 0, len(wf.Steps)) @@ -87,7 +87,7 @@ func resolveSteps(orgID string, wf *models.Workflow) ([]models.ResolvedStep, err out = append(out, resolveInlineStep(ref)) continue } - lib, err := getStep(ctx, orgID, ref.StepID) + lib, err := getStep(ctx, instanceID, ref.StepID) if err != nil { return nil, err } @@ -163,7 +163,7 @@ func executeRun(runID string) { done := make(chan int, len(run.ServerRuns)) for i := range run.ServerRuns { go func(idx int) { - runServer(run.OrgID, runID, idx, run.Steps, run.ServerRuns[idx].ServerID) + runServer(run.InstanceID, runID, idx, run.Steps, run.ServerRuns[idx].ServerID) done <- idx }(i) } @@ -185,7 +185,7 @@ func executeRun(runID string) { bson.M{"$set": bson.M{"status": status, "finished_at": now}}) } -func runServer(orgID, runID string, srvIdx int, steps []models.ResolvedStep, serverID string) { +func runServer(instanceID, 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}) @@ -212,7 +212,7 @@ func runServer(orgID, runID string, srvIdx int, steps []models.ResolvedStep, ser maxAttempts = step.MaxRetries + 1 } - secretVals := resolveSecrets(orgID, step.SecretRefs) + secretVals := resolveSecrets(instanceID, step.SecretRefs) for k, v := range secretVals { allSecrets[k] = v } @@ -347,7 +347,7 @@ func expandVars(v string, lookup map[string]string) string { }) } -func resolveSecrets(orgID string, refs []string) map[string]string { +func resolveSecrets(instanceID string, refs []string) map[string]string { out := map[string]string{} for _, ref := range refs { @@ -355,7 +355,7 @@ func resolveSecrets(orgID string, refs []string) map[string]string { if len(parts) != 2 { continue } - if v, err := RevealSecret(orgID, parts[0], parts[1]); err == nil { + if v, err := RevealSecret(instanceID, parts[0], parts[1]); err == nil { out[parts[1]] = v } } @@ -453,21 +453,21 @@ func getRunByID(runID string) (*models.WorkflowRun, error) { return &r, err } -func GetRun(orgID, runID string) (*models.WorkflowRun, error) { +func GetRun(instanceID, runID string) (*models.WorkflowRun, error) { ctx, cancel := wfCtx() defer cancel() var r models.WorkflowRun - err := db.Col("workflow_runs").FindOne(ctx, bson.M{"run_id": runID, "org_id": orgID}).Decode(&r) + err := db.Col("workflow_runs").FindOne(ctx, bson.M{"run_id": runID, "instance_id": instanceID}).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) { +func ListRuns(instanceID, 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}, + cur, err := db.Col("workflow_runs").Find(ctx, bson.M{"instance_id": instanceID, "workflow_id": workflowID}, options.Find().SetSort(bson.D{{Key: "started_at", Value: -1}}).SetLimit(limit)) if err != nil { return nil, err @@ -480,12 +480,12 @@ func ListRuns(orgID, workflowID string, limit int64) ([]models.WorkflowRun, erro return runs, nil } -func CancelRun(orgID, runID string) error { +func CancelRun(instanceID, runID string) error { now := time.Now() ctx, cancel := wfCtx() defer cancel() _, err := db.Col("workflow_runs").UpdateOne(ctx, - bson.M{"org_id": orgID, "run_id": runID, "status": "running"}, + bson.M{"instance_id": instanceID, "run_id": runID, "status": "running"}, bson.M{"$set": bson.M{"status": "cancelled", "finished_at": now}}) return err } diff --git a/server/internal/services/workflows.go b/server/internal/services/workflows.go index c6d7891..4b6abf4 100644 --- a/server/internal/services/workflows.go +++ b/server/internal/services/workflows.go @@ -25,13 +25,12 @@ func EnsureWorkflowIndexes() error { }); err != nil { return err } - - + if err := db.Col("workflow_steps").Indexes().DropOne(ctx, "slug_1"); err != nil && !isIndexNotFound(err) { return err } if _, err := db.Col("workflow_steps").Indexes().CreateOne(ctx, mongo.IndexModel{ - Keys: bson.D{{Key: "org_id", Value: 1}, {Key: "slug", Value: 1}}, + Keys: bson.D{{Key: "instance_id", Value: 1}, {Key: "slug", Value: 1}}, Options: options.Index().SetUnique(true). SetPartialFilterExpression(bson.M{"source": "default"}), }); err != nil { @@ -48,12 +47,10 @@ func EnsureWorkflowIndexes() error { return err } - - -func ListSteps(orgID string) ([]models.WorkflowStep, error) { +func ListSteps(instanceID string) ([]models.WorkflowStep, error) { ctx, cancel := wfCtx() defer cancel() - cur, err := db.Col("workflow_steps").Find(ctx, bson.M{"org_id": orgID}, + cur, err := db.Col("workflow_steps").Find(ctx, bson.M{"instance_id": instanceID}, options.Find().SetSort(bson.D{{Key: "name", Value: 1}})) if err != nil { return nil, err @@ -66,12 +63,10 @@ func ListSteps(orgID string) ([]models.WorkflowStep, error) { return steps, nil } - - -func StepUsageCounts(orgID string) (map[string]int, error) { +func StepUsageCounts(instanceID string) (map[string]int, error) { ctx, cancel := wfCtx() defer cancel() - cur, err := db.Col("workflows").Find(ctx, bson.M{"org_id": orgID}) + cur, err := db.Col("workflows").Find(ctx, bson.M{"instance_id": instanceID}) if err != nil { return nil, err } @@ -94,10 +89,10 @@ func StepUsageCounts(orgID string) (map[string]int, error) { return counts, nil } -func CreateStep(orgID string, s models.WorkflowStep) (*models.WorkflowStep, error) { +func CreateStep(instanceID string, s models.WorkflowStep) (*models.WorkflowStep, error) { ctx, cancel := wfCtx() defer cancel() - s.OrgID = orgID + s.InstanceID = instanceID s.StepID = uuid.New().String() s.CreatedAt = time.Now() s.UpdatedAt = s.CreatedAt @@ -117,10 +112,10 @@ func CreateStep(orgID string, s models.WorkflowStep) (*models.WorkflowStep, erro return &s, nil } -func UpdateStep(orgID, stepID string, s models.WorkflowStep) error { +func UpdateStep(instanceID, stepID string, s models.WorkflowStep) error { ctx, cancel := wfCtx() defer cancel() - _, err := db.Col("workflow_steps").UpdateOne(ctx, bson.M{"step_id": stepID, "org_id": orgID}, bson.M{"$set": bson.M{ + _, err := db.Col("workflow_steps").UpdateOne(ctx, bson.M{"step_id": stepID, "instance_id": instanceID}, bson.M{"$set": bson.M{ "name": s.Name, "description": s.Description, "interpreter": s.Interpreter, @@ -133,14 +128,14 @@ func UpdateStep(orgID, stepID string, s models.WorkflowStep) error { return err } -func DeleteStep(orgID, stepID string) error { +func DeleteStep(instanceID, stepID string) error { ctx, cancel := wfCtx() defer cancel() - if _, err := db.Col("workflow_steps").DeleteOne(ctx, bson.M{"step_id": stepID, "org_id": orgID}); err != nil { + if _, err := db.Col("workflow_steps").DeleteOne(ctx, bson.M{"step_id": stepID, "instance_id": instanceID}); err != nil { return err } - - cur, err := db.Col("workflows").Find(ctx, bson.M{"steps.step_id": stepID, "org_id": orgID}) + + cur, err := db.Col("workflows").Find(ctx, bson.M{"steps.step_id": stepID, "instance_id": instanceID}) if err != nil { return err } @@ -170,21 +165,19 @@ func DeleteStep(orgID, stepID string) error { return nil } -func getStep(ctx context.Context, orgID, stepID string) (*models.WorkflowStep, error) { +func getStep(ctx context.Context, instanceID, stepID string) (*models.WorkflowStep, error) { var s models.WorkflowStep - err := db.Col("workflow_steps").FindOne(ctx, bson.M{"step_id": stepID, "org_id": orgID}).Decode(&s) + err := db.Col("workflow_steps").FindOne(ctx, bson.M{"step_id": stepID, "instance_id": instanceID}).Decode(&s) if err == mongo.ErrNoDocuments { return nil, fmt.Errorf("step %s not found", stepID) } return &s, err } - - -func ListWorkflows(orgID string) ([]models.Workflow, error) { +func ListWorkflows(instanceID string) ([]models.Workflow, error) { ctx, cancel := wfCtx() defer cancel() - cur, err := db.Col("workflows").Find(ctx, bson.M{"org_id": orgID}, + cur, err := db.Col("workflows").Find(ctx, bson.M{"instance_id": instanceID}, options.Find().SetSort(bson.D{{Key: "name", Value: 1}})) if err != nil { return nil, err @@ -197,21 +190,21 @@ func ListWorkflows(orgID string) ([]models.Workflow, error) { return wfs, nil } -func GetWorkflow(orgID, id string) (*models.Workflow, error) { +func GetWorkflow(instanceID, 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, "org_id": orgID}).Decode(&w) + err := db.Col("workflows").FindOne(ctx, bson.M{"workflow_id": id, "instance_id": instanceID}).Decode(&w) if err == mongo.ErrNoDocuments { return nil, fmt.Errorf("workflow not found") } return &w, err } -func CreateWorkflow(orgID string, w models.Workflow) (*models.Workflow, error) { +func CreateWorkflow(instanceID string, w models.Workflow) (*models.Workflow, error) { ctx, cancel := wfCtx() defer cancel() - w.OrgID = orgID + w.InstanceID = instanceID w.WorkflowID = uuid.New().String() w.CreatedAt = time.Now() w.UpdatedAt = w.CreatedAt @@ -224,7 +217,7 @@ func CreateWorkflow(orgID string, w models.Workflow) (*models.Workflow, error) { if err := ValidateWorkflow(w); err != nil { return nil, err } - if err := validateTargetServers(orgID, w.TargetServerIDs); err != nil { + if err := validateTargetServers(instanceID, w.TargetServerIDs); err != nil { return nil, err } normalizeInlineSteps(&w) @@ -234,17 +227,17 @@ func CreateWorkflow(orgID string, w models.Workflow) (*models.Workflow, error) { return &w, nil } -func UpdateWorkflow(orgID, id string, w models.Workflow) error { +func UpdateWorkflow(instanceID, id string, w models.Workflow) error { ctx, cancel := wfCtx() defer cancel() if err := ValidateWorkflow(w); err != nil { return err } - if err := validateTargetServers(orgID, w.TargetServerIDs); err != nil { + if err := validateTargetServers(instanceID, w.TargetServerIDs); err != nil { return err } normalizeInlineSteps(&w) - _, err := db.Col("workflows").UpdateOne(ctx, bson.M{"workflow_id": id, "org_id": orgID}, bson.M{"$set": bson.M{ + _, err := db.Col("workflows").UpdateOne(ctx, bson.M{"workflow_id": id, "instance_id": instanceID}, bson.M{"$set": bson.M{ "name": w.Name, "target_server_ids": w.TargetServerIDs, "steps": w.Steps, @@ -253,20 +246,15 @@ func UpdateWorkflow(orgID, id string, w models.Workflow) error { return err } - - - -func validateTargetServers(orgID string, serverIDs []string) error { +func validateTargetServers(instanceID string, serverIDs []string) error { for _, sid := range serverIDs { - if _, err := GetServer(orgID, sid); err != nil { + if _, err := GetServer(instanceID, sid); err != nil { return fmt.Errorf("target server %s not found", sid) } } return nil } - - func normalizeInlineSteps(w *models.Workflow) { for i := range w.Steps { in := w.Steps[i].Inline @@ -288,9 +276,9 @@ func normalizeInlineSteps(w *models.Workflow) { } } -func DeleteWorkflow(orgID, id string) error { +func DeleteWorkflow(instanceID, id string) error { ctx, cancel := wfCtx() defer cancel() - _, err := db.Col("workflows").DeleteOne(ctx, bson.M{"workflow_id": id, "org_id": orgID}) + _, err := db.Col("workflows").DeleteOne(ctx, bson.M{"workflow_id": id, "instance_id": instanceID}) return err }