Cluster: Improve API endpoint and CLI command logs

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-10-21 16:51:24 +02:00
parent 5b7ea44eb5
commit 53f7643583
8 changed files with 22 additions and 22 deletions

View File

@@ -33,7 +33,7 @@ The API package exposes PhotoPrisms HTTP endpoints via Gin handlers. Each fil
- 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.
- **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 such as `status.Succeeded`, `status.Failed`, `status.Denied`, or `status.Error(err)` when the sanitized error message should be the outcome; 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:
@@ -42,7 +42,7 @@ The API package exposes PhotoPrisms HTTP endpoints via Gin handlers. Each fil
ClientIP(c),
"session %s",
string(acl.ResourceCluster),
"node %s",
"node", "%s",
status.Deleted,
}, s.RefID, uuid)

View File

@@ -174,7 +174,7 @@ func ClusterGetNode(router *gin.RouterGroup) {
// Audit get access.
event.AuditInfo(
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "get node %s", status.Succeeded},
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "get node", "%s", status.Succeeded},
s.RefID,
uuid,
)
@@ -262,7 +262,7 @@ func ClusterUpdateNode(router *gin.RouterGroup) {
}
event.AuditInfo(
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "node %s", status.Updated},
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "node", "%s", status.Updated},
s.RefID,
uuid,
)
@@ -321,7 +321,7 @@ func ClusterDeleteNode(router *gin.RouterGroup) {
}
event.AuditWarn(
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "node %s", status.Deleted},
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "node", "%s", status.Deleted},
s.RefID,
uuid,
)

View File

@@ -155,7 +155,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", "invalid client secret", status.Denied}, clean.Log(name))
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "node", "%s", "invalid client secret", status.Denied}, clean.Log(name))
c.JSON(http.StatusConflict, gin.H{"error": "client secret required to change node uuid"})
return
}
@@ -187,17 +187,17 @@ 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", status.Updated}, clean.Log(name), clean.Log(oldUUID), clean.Log(requestedUUID))
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node", "%s", "change uuid old %s new %s", status.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", "assign uuid %s", status.Created}, clean.Log(name), clean.Log(n.UUID))
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node", "%s", "assign uuid %s", status.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", status.Error(putErr)}, clean.Log(name))
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node", "%s", "persist node", status.Error(putErr)}, clean.Log(name))
AbortUnexpectedError(c)
return
}
@@ -206,16 +206,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", status.Error(err)}, clean.Log(name))
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node", "%s", "rotate secret", status.Error(err)}, clean.Log(name))
AbortUnexpectedError(c)
return
}
respSecret = &cluster.RegisterSecrets{ClientSecret: n.ClientSecret, RotatedAt: n.RotatedAt}
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "rotate secret", status.Succeeded}, clean.Log(name))
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node", "%s", "rotate secret", status.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", status.Error(putErr)}, clean.Log(name))
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node", "%s", "persist rotated secret", status.Error(putErr)}, clean.Log(name))
AbortUnexpectedError(c)
return
}
@@ -230,7 +230,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", status.Error(credsErr)}, clean.Log(name))
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "node", "%s", "ensure database", status.Error(credsErr)}, clean.Log(name))
c.JSON(http.StatusConflict, gin.H{"error": credsErr.Error()})
return
}
@@ -240,11 +240,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", status.Error(putErr)}, clean.Log(name))
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node", "%s", "persist node", status.Error(putErr)}, clean.Log(name))
AbortUnexpectedError(c)
return
}
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "rotate database", status.Succeeded}, clean.Log(name))
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node", "%s", "rotate database", status.Succeeded}, clean.Log(name))
}
}
@@ -278,7 +278,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", status.Synced}, clean.Log(name))
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node", "%s", status.Synced}, clean.Log(name))
c.JSON(http.StatusOK, resp)
return
}
@@ -360,7 +360,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", status.Joined}, clean.Log(name))
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node", "%s", status.Joined}, clean.Log(name))
c.JSON(http.StatusCreated, resp)
})
}

View File

@@ -125,7 +125,7 @@ func clusterNodesModAction(ctx *cli.Context) error {
who := clusterAuditWho(ctx, conf)
segments := []string{
string(acl.ResourceCluster),
"update node %s",
"update node", "%s",
}
args := []interface{}{clean.Log(nodeID)}

View File

@@ -115,7 +115,7 @@ func clusterNodesRemoveAction(ctx *cli.Context) error {
who := clusterAuditWho(ctx, conf)
event.AuditInfo(append(who,
string(acl.ResourceCluster),
"node %s",
"node", "%s",
status.Deleted,
), clean.Log(uuid))

View File

@@ -192,7 +192,7 @@ func clusterNodesRotateAction(ctx *cli.Context) error {
who := clusterAuditWho(ctx, conf)
segments := []string{
string(acl.ResourceCluster),
"rotate node %s",
"rotate node", "%s",
}
args := []interface{}{clean.Log(nodeID)}
if detail != "" {

View File

@@ -63,7 +63,7 @@ func clusterNodesShowAction(ctx *cli.Context) error {
who := clusterAuditWho(ctx, conf)
event.AuditInfo(append(who,
string(acl.ResourceCluster),
"show node %s",
"show node", "%s",
status.Succeeded,
), clean.Log(dto.UUID))

View File

@@ -304,7 +304,7 @@ func clusterRegisterAction(ctx *cli.Context) error {
who := clusterAuditWho(ctx, conf)
event.AuditInfo(append(who,
string(acl.ResourceCluster),
"register node %s",
"register node", "%s",
status.Succeeded,
), clean.Log(nodeID))