mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
API: Improve audit log messages in the cluster endpoint handlers
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -29,6 +29,34 @@ The API package exposes PhotoPrism’s HTTP endpoints via Gin handlers. Each fil
|
||||
- Derive client IPs through `api.ClientIP` and extract bearer tokens with `header.BearerToken` or the helper setters. Use constant-time comparison for tokens and secrets.
|
||||
- For downloads or proxy endpoints, validate URLs against allowed schemes (`http`, `https`) and reject private or loopback addresses unless explicitly required.
|
||||
|
||||
## Audit Logging
|
||||
|
||||
- Emit security events via `event.Audit*` (`AuditInfo`, `AuditWarn`, `AuditErr`, `AuditDebug`) and always build the slice as **Who → What → Outcome**.
|
||||
- **Who:** `ClientIP(c)` followed by the most specific actor context (`"session %s"`, `"client %s"`, `"user %s"`).
|
||||
- **What:** Resource constant plus action segments (for example, `string(acl.ResourceCluster)`, `"node %s"`). Place extra context such as counts or error placeholders in separate segments before the outcome.
|
||||
- **Outcome:** End with a single token like `event.Succeeded`, `event.Failed`, or `authn.Denied`; nothing comes after it.
|
||||
- Prefer existing helpers (`ClientIP`, `clean.Log`, `clean.LogQuote`, `clean.Error`) instead of formatting values manually, and avoid inline `=` expressions.
|
||||
- Example patterns:
|
||||
```go
|
||||
event.AuditInfo([]string{
|
||||
ClientIP(c),
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"node %s",
|
||||
event.Deleted,
|
||||
}, s.RefID, uuid)
|
||||
|
||||
event.AuditErr([]string{
|
||||
clientIp,
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"download theme",
|
||||
"%s",
|
||||
event.Failed,
|
||||
}, refID, clean.Error(err))
|
||||
```
|
||||
- See `specs/common/audit-logs.md` for the full conventions and additional examples that agents should follow.
|
||||
|
||||
## Swagger Documentation
|
||||
|
||||
- Annotate handlers with Swagger comments that include full `/api/v1/...` paths, request/response schemas, and security definitions. Only annotate routes that are externally accessible.
|
||||
|
||||
@@ -50,11 +50,13 @@ func ClusterMetrics(router *gin.RouterGroup) {
|
||||
counts[role]++
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, cluster.MetricsResponse{
|
||||
resp := cluster.MetricsResponse{
|
||||
UUID: conf.ClusterUUID(),
|
||||
ClusterCIDR: conf.ClusterCIDR(),
|
||||
Nodes: counts,
|
||||
Time: time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,7 +108,14 @@ func ClusterListNodes(router *gin.RouterGroup) {
|
||||
resp := reg.BuildClusterNodes(page, opts)
|
||||
|
||||
// Audit list access.
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "nodes", "list", event.Succeeded, "count=%d", "offset=%d", "returned=%d"}, s.RefID, count, offset, len(resp))
|
||||
event.AuditDebug([]string{
|
||||
ClientIP(c),
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"list nodes",
|
||||
"count %d offset %d returned %d",
|
||||
event.Succeeded,
|
||||
}, s.RefID, count, offset, len(resp))
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
})
|
||||
@@ -166,7 +173,13 @@ func ClusterGetNode(router *gin.RouterGroup) {
|
||||
resp := reg.BuildClusterNode(*n, opts)
|
||||
|
||||
// Audit get access.
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "nodes", "get", uuid, event.Succeeded}, s.RefID)
|
||||
event.AuditInfo([]string{
|
||||
ClientIP(c),
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"get node %s",
|
||||
event.Succeeded,
|
||||
}, s.RefID, uuid)
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
})
|
||||
@@ -238,8 +251,9 @@ func ClusterUpdateNode(router *gin.RouterGroup) {
|
||||
if req.AdvertiseUrl != "" {
|
||||
n.AdvertiseUrl = req.AdvertiseUrl
|
||||
}
|
||||
if s := normalizeSiteURL(req.SiteUrl); s != "" {
|
||||
n.SiteUrl = s
|
||||
|
||||
if u := normalizeSiteURL(req.SiteUrl); u != "" {
|
||||
n.SiteUrl = u
|
||||
}
|
||||
|
||||
n.UpdatedAt = time.Now().UTC().Format(time.RFC3339)
|
||||
@@ -249,7 +263,14 @@ func ClusterUpdateNode(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditInfo([]string{ClientIP(c), string(acl.ResourceCluster), "nodes", "update", uuid, event.Succeeded})
|
||||
event.AuditInfo([]string{
|
||||
ClientIP(c),
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"node %s",
|
||||
event.Updated,
|
||||
}, s.RefID, uuid)
|
||||
|
||||
c.JSON(http.StatusOK, cluster.StatusResponse{Status: "ok"})
|
||||
})
|
||||
}
|
||||
@@ -303,7 +324,14 @@ func ClusterDeleteNode(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditInfo([]string{ClientIP(c), string(acl.ResourceCluster), "nodes", "delete", uuid, event.Succeeded})
|
||||
event.AuditWarn([]string{
|
||||
ClientIP(c),
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"node %s",
|
||||
event.Deleted,
|
||||
}, s.RefID, uuid)
|
||||
|
||||
c.JSON(http.StatusOK, cluster.StatusResponse{Status: "ok"})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
r := limiter.Auth.Request(clientIp)
|
||||
|
||||
if r.Reject() || limiter.Auth.Reject(clientIp) {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register node", "rate limit", event.Denied})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "rate limit exceeded", event.Denied})
|
||||
limiter.AbortJSON(c)
|
||||
return
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
token := header.BearerToken(c)
|
||||
|
||||
if expected == "" || token == "" || subtle.ConstantTimeCompare([]byte(expected), []byte(token)) != 1 {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register node", "auth", event.Denied})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid join token", event.Denied})
|
||||
r.Success() // return reserved tokens; still unauthorized
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
@@ -73,7 +73,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
var req cluster.RegisterRequest
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register node", "form invalid", "%s"}, clean.Error(err))
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid form", "%s", event.Failed}, clean.Error(err))
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
@@ -85,13 +85,13 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
// If an existing ClientID is provided, require the corresponding client secret for verification.
|
||||
if RegisterRequireClientSecret && req.ClientID != "" {
|
||||
if !rnd.IsUID(req.ClientID, entity.ClientUID) {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register node", "invalid client id"})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid client id", event.Failed})
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
pw := entity.FindPassword(req.ClientID)
|
||||
if pw == nil || req.ClientSecret == "" || !pw.Valid(req.ClientSecret) {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register node", "invalid client secret"})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid client secret", event.Denied})
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
@@ -101,14 +101,14 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
|
||||
// Enforce DNS label semantics for node names: lowercase [a-z0-9-], 1–32, start/end alnum.
|
||||
if name == "" || len(name) > 32 || name[0] == '-' || name[len(name)-1] == '-' {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register node", "invalid name"})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid name", event.Failed})
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
for i := 0; i < len(name); i++ {
|
||||
b := name[i]
|
||||
if !(b == '-' || (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9')) {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register node", "invalid name chars"})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid name chars", event.Failed})
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
// Validate advertise URL if provided (https required for non-local domains).
|
||||
if u := strings.TrimSpace(req.AdvertiseUrl); u != "" {
|
||||
if !validateAdvertiseURL(u) {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register node", "invalid advertise url"})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid advertise url", event.Failed})
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
// Validate site URL if provided (https required for non-local domains).
|
||||
if su := strings.TrimSpace(req.SiteUrl); su != "" {
|
||||
if !validateSiteURL(su) {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register node", "invalid site url"})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid site url", event.Failed})
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
@@ -139,7 +139,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
regy, err := reg.NewClientRegistryWithConfig(conf)
|
||||
|
||||
if err != nil {
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "register node", "registry", event.Failed, "%s"}, clean.Error(err))
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "register", "%s", event.Failed}, clean.Error(err))
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
@@ -154,7 +154,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
// If caller attempts to change UUID by name without proving client secret, block with 409.
|
||||
if RegisterRequireClientSecret {
|
||||
if requestedUUID != "" && n.UUID != "" && requestedUUID != n.UUID && req.ClientID == "" {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "node %s", "uuid change requires client secret", event.Denied}, clean.LogQuote(name))
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "node %s uuid change requires client secret", event.Denied}, clean.Log(name))
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "client secret required to change node uuid"})
|
||||
return
|
||||
}
|
||||
@@ -184,16 +184,16 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
if oldUUID != requestedUUID {
|
||||
n.UUID = requestedUUID
|
||||
// Emit audit event for UUID change.
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "change uuid", "old %s", "new %s", event.Succeeded}, clean.LogQuote(name), clean.Log(oldUUID), clean.Log(requestedUUID))
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "change uuid old %s new %s", event.Updated}, clean.Log(name), clean.Log(oldUUID), clean.Log(requestedUUID))
|
||||
}
|
||||
} else if n.UUID == "" {
|
||||
// Assign a fresh UUID if missing and none requested.
|
||||
n.UUID = rnd.UUIDv7()
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "new uuid", "%s", event.Succeeded}, clean.LogQuote(name), clean.Log(n.UUID))
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "assign uuid %s", event.Created}, clean.Log(name), clean.Log(n.UUID))
|
||||
}
|
||||
// Persist metadata changes so UpdatedAt advances.
|
||||
if putErr := regy.Put(n); putErr != nil {
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node %s", "persist node", "%s", event.Failed}, clean.LogQuote(name), clean.Error(putErr))
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node %s", "persist node", "%s", event.Failed}, clean.Log(name), clean.Error(putErr))
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
@@ -201,16 +201,16 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
var respSecret *cluster.RegisterSecrets
|
||||
if req.RotateSecret {
|
||||
if n, err = regy.RotateSecret(n.UUID); err != nil {
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node %s", "rotate secret", "%s", event.Failed}, clean.LogQuote(name), clean.Error(err))
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node %s", "rotate secret", "%s", event.Failed}, clean.Log(name), clean.Error(err))
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
respSecret = &cluster.RegisterSecrets{ClientSecret: n.ClientSecret, RotatedAt: n.RotatedAt}
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "rotate secret", event.Succeeded}, clean.LogQuote(name))
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s rotate secret", event.Succeeded}, clean.Log(name))
|
||||
|
||||
// Extra safety: ensure the updated secret is persisted even if subsequent steps fail.
|
||||
if putErr := regy.Put(n); putErr != nil {
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node %s", "persist rotated secret", "%s", event.Failed}, clean.LogQuote(name), clean.Error(putErr))
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node %s", "persist rotated secret", "%s", event.Failed}, clean.Log(name), clean.Error(putErr))
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
@@ -225,7 +225,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
creds, _, credsErr = provisioner.EnsureCredentials(c, conf, n.UUID, name, req.RotateDatabase)
|
||||
|
||||
if credsErr != nil {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "node %s", "ensure database", "%s", event.Failed}, clean.LogQuote(name), clean.Error(credsErr))
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "node %s", "ensure database", "%s", event.Failed}, clean.Log(name), clean.Error(credsErr))
|
||||
c.JSON(http.StatusConflict, gin.H{"error": credsErr.Error()})
|
||||
return
|
||||
}
|
||||
@@ -235,11 +235,11 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
n.Database.RotatedAt = creds.RotatedAt
|
||||
n.Database.Driver = provisioner.DatabaseDriver
|
||||
if putErr := regy.Put(n); putErr != nil {
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node %s", "persist node", "%s", event.Failed}, clean.LogQuote(name), clean.Error(putErr))
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node %s", "persist node", "%s", event.Failed}, clean.Log(name), clean.Error(putErr))
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "rotate db", event.Succeeded}, clean.LogQuote(name))
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s rotate database", event.Succeeded}, clean.Log(name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
resp.Database.RotatedAt = creds.RotatedAt
|
||||
}
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "registration", event.Updated}, clean.LogQuote(name))
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", event.Updated}, clean.Log(name))
|
||||
c.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
@@ -315,7 +315,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
|
||||
if shouldProvisionDB {
|
||||
if creds, _, err = provisioner.EnsureCredentials(c, conf, n.UUID, name, true); err != nil {
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register node", "ensure database", "%s", event.Failed}, clean.Error(err))
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "ensure database", "%s", event.Failed}, clean.Error(err))
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -329,7 +329,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
if err = regy.Put(n); err != nil {
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "register node", "persist node", "%s", event.Failed}, clean.Error(err))
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "register", "persist node", "%s", event.Failed}, clean.Error(err))
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
@@ -355,7 +355,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
// When DB provisioning is skipped, leave Database fields zero-value.
|
||||
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "registration", event.Created}, clean.LogQuote(name))
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", event.Created}, clean.Log(name))
|
||||
c.JSON(http.StatusCreated, resp)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/auth/acl"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
"github.com/photoprism/photoprism/internal/service/cluster"
|
||||
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
|
||||
@@ -51,14 +52,24 @@ func ClusterSummary(router *gin.RouterGroup) {
|
||||
themeVersion = v
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, cluster.SummaryResponse{
|
||||
resp := cluster.SummaryResponse{
|
||||
UUID: conf.ClusterUUID(),
|
||||
ClusterCIDR: conf.ClusterCIDR(),
|
||||
Nodes: len(nodes),
|
||||
Database: cluster.DatabaseInfo{Driver: conf.DatabaseDriverName(), Host: conf.DatabaseHost(), Port: conf.DatabasePort()},
|
||||
Theme: themeVersion,
|
||||
Time: time.Now().UTC().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
event.AuditDebug([]string{
|
||||
ClientIP(c),
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"get summary for cluster uuid %s",
|
||||
event.Succeeded,
|
||||
}, s.RefID, conf.ClusterUUID())
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -85,6 +96,13 @@ func ClusterHealth(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditDebug([]string{
|
||||
ClientIP(c),
|
||||
string(acl.ResourceCluster),
|
||||
"health check",
|
||||
event.Succeeded,
|
||||
})
|
||||
|
||||
c.JSON(http.StatusOK, NewHealthResponse("ok"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func ClusterGetTheme(router *gin.RouterGroup) {
|
||||
|
||||
// Resolve symbolic links.
|
||||
if resolved, err := filepath.EvalSymlinks(themePath); err != nil {
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "download", "%s"}, refID, clean.Error(err))
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme resolve", "%s", event.Failed}, refID, clean.Error(err))
|
||||
AbortNotFound(c)
|
||||
return
|
||||
} else {
|
||||
@@ -84,7 +84,7 @@ func ClusterGetTheme(router *gin.RouterGroup) {
|
||||
|
||||
// Check if theme path exists.
|
||||
if !fs.PathExists(themePath) {
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "download", "theme path not found"}, refID)
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme path", "not found"}, refID)
|
||||
AbortNotFound(c)
|
||||
return
|
||||
}
|
||||
@@ -93,17 +93,17 @@ func ClusterGetTheme(router *gin.RouterGroup) {
|
||||
// This aligns with bootstrap behavior, which only installs a theme when
|
||||
// app.js exists locally or can be fetched from the Portal.
|
||||
if !fs.FileExistsNotEmpty(filepath.Join(themePath, "app.js")) {
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "download", "app.js missing or empty"}, refID)
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme app.js", "not found"}, refID)
|
||||
AbortNotFound(c)
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "download", "creating theme archive from %s"}, refID, clean.Log(themePath))
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme create archive", "%s", "started"}, refID, clean.Log(themePath))
|
||||
|
||||
if version, err := theme.DetectVersion(themePath); err == nil {
|
||||
updateNodeThemeVersion(conf, session, version, clientIp, refID)
|
||||
} else {
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "version", "%s"}, refID, clean.Error(err))
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "detect theme version", "%s", event.Failed}, refID, clean.Error(err))
|
||||
}
|
||||
|
||||
// Add response headers.
|
||||
@@ -114,14 +114,14 @@ func ClusterGetTheme(router *gin.RouterGroup) {
|
||||
zipWriter := zip.NewWriter(c.Writer)
|
||||
defer func(w *zip.Writer) {
|
||||
if closeErr := w.Close(); closeErr != nil {
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "download", "failed to close", "%s"}, refID, clean.Error(closeErr))
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme close", "%s", event.Failed}, refID, clean.Error(closeErr))
|
||||
}
|
||||
}(zipWriter)
|
||||
|
||||
err := filepath.WalkDir(themePath, func(filePath string, info gofs.DirEntry, walkErr error) error {
|
||||
// Handle errors.
|
||||
if walkErr != nil {
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "download", "failed to traverse theme path", "%s"}, refID, clean.Error(walkErr))
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme traverse", "%s", event.Failed}, refID, clean.Error(walkErr))
|
||||
|
||||
// If the error occurs on a directory, skip descending to avoid cascading errors.
|
||||
if info != nil && info.IsDir() {
|
||||
@@ -157,11 +157,11 @@ func ClusterGetTheme(router *gin.RouterGroup) {
|
||||
// Get the relative file name to use as alias in the zip.
|
||||
alias := filepath.ToSlash(fs.RelName(filePath, themePath))
|
||||
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "download", "adding %s to archive"}, refID, clean.Log(alias))
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme add", "%s", event.Added}, refID, clean.Log(alias))
|
||||
|
||||
// Stream zipped file contents.
|
||||
if zipErr := fs.ZipFile(zipWriter, filePath, alias, false); zipErr != nil {
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "download", "failed to add %s", "%s"}, refID, clean.Log(alias), clean.Error(zipErr))
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme add %s", "%s", event.Failed}, refID, clean.Log(alias), clean.Error(zipErr))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -169,9 +169,9 @@ func ClusterGetTheme(router *gin.RouterGroup) {
|
||||
|
||||
// Log result.
|
||||
if err != nil {
|
||||
event.AuditErr([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "download", event.Failed, "%s"}, refID, clean.Error(err))
|
||||
event.AuditErr([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme", "%s", event.Failed}, refID, clean.Error(err))
|
||||
} else {
|
||||
event.AuditInfo([]string{clientIp, "session %s", string(acl.ResourceCluster), "theme", "download", event.Succeeded}, refID)
|
||||
event.AuditInfo([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme", event.Succeeded}, refID)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -198,7 +198,7 @@ func updateNodeThemeVersion(conf *config.Config, session *entity.Session, versio
|
||||
regy, err := reg.NewClientRegistryWithConfig(conf)
|
||||
|
||||
if err != nil {
|
||||
event.AuditDebug([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme", "metadata", "registry", "%s"}, refID, clean.Error(err))
|
||||
event.AuditDebug([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme metadata registry", "%s", event.Failed}, refID, clean.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ func updateNodeThemeVersion(conf *config.Config, session *entity.Session, versio
|
||||
}
|
||||
|
||||
if node == nil {
|
||||
event.AuditDebug([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme", "metadata", "node not found"}, refID)
|
||||
event.AuditDebug([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme metadata node", "skipped"}, refID)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,9 +228,9 @@ func updateNodeThemeVersion(conf *config.Config, session *entity.Session, versio
|
||||
node.Theme = normalized
|
||||
|
||||
if err = regy.Put(node); err != nil {
|
||||
event.AuditWarn([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme", "metadata", "%s"}, refID, clean.Error(err))
|
||||
event.AuditWarn([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme metadata", "%s", event.Failed}, refID, clean.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditDebug([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme", "metadata", "updated"}, refID)
|
||||
event.AuditDebug([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme metadata", event.Updated}, refID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user