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.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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
|
||||
|
||||
+19
-23
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
@@ -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:"-"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 != ""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user