From c5b5feee470b900d6b4e4ce2ad1778647b5e2e0a Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Mon, 20 Oct 2025 14:49:25 +0200 Subject: [PATCH] CLI: Add audit logs to cluster management commands Signed-off-by: Michael Mayer --- internal/commands/cluster_helpers.go | 24 +++++++++++++++++ internal/commands/cluster_nodes_list.go | 9 +++++++ internal/commands/cluster_nodes_mod.go | 25 ++++++++++++++++++ internal/commands/cluster_nodes_remove.go | 14 ++++++++++ internal/commands/cluster_nodes_rotate.go | 32 +++++++++++++++++++++++ internal/commands/cluster_nodes_show.go | 9 +++++++ internal/commands/cluster_register.go | 14 ++++++++++ 7 files changed, 127 insertions(+) diff --git a/internal/commands/cluster_helpers.go b/internal/commands/cluster_helpers.go index 453a18bfa..71c2b7de1 100644 --- a/internal/commands/cluster_helpers.go +++ b/internal/commands/cluster_helpers.go @@ -8,7 +8,11 @@ import ( "net/url" "strings" + "github.com/urfave/cli/v2" + + "github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/service/cluster" + "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/http/header" ) @@ -53,3 +57,23 @@ func obtainClientCredentialsViaRegister(portalURL, joinToken, nodeName string) ( } return id, secret, nil } + +// clusterAuditWho builds the leading audit log segments for CLI commands. +func clusterAuditWho(ctx *cli.Context, conf *config.Config) []string { + actor := clean.Log(conf.NodeName()) + if actor == "" { + actor = clean.Log(conf.SiteUrl()) + } + if actor == "" { + actor = "cli" + } + + context := "cli" + if ctx != nil && ctx.Command != nil { + if full := strings.TrimSpace(ctx.Command.FullName()); full != "" { + context = "cli " + full + } + } + + return []string{actor, context} +} diff --git a/internal/commands/cluster_nodes_list.go b/internal/commands/cluster_nodes_list.go index e65ed9666..54216eeee 100644 --- a/internal/commands/cluster_nodes_list.go +++ b/internal/commands/cluster_nodes_list.go @@ -7,7 +7,9 @@ import ( "github.com/urfave/cli/v2" + "github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/event" reg "github.com/photoprism/photoprism/internal/service/cluster/registry" "github.com/photoprism/photoprism/pkg/txt/report" ) @@ -74,6 +76,13 @@ func clusterNodesListAction(ctx *cli.Context) error { opts := reg.NodeOpts{IncludeAdvertiseUrl: true, IncludeDatabase: true} out := reg.BuildClusterNodes(page, opts) + who := clusterAuditWho(ctx, conf) + event.AuditInfo(append(who, + string(acl.ResourceCluster), + "list nodes count %d", + event.Succeeded, + ), len(out)) + if ctx.Bool("json") { b, _ := json.Marshal(out) fmt.Println(string(b)) diff --git a/internal/commands/cluster_nodes_mod.go b/internal/commands/cluster_nodes_mod.go index 69e2dbdfd..461aa48a5 100644 --- a/internal/commands/cluster_nodes_mod.go +++ b/internal/commands/cluster_nodes_mod.go @@ -7,7 +7,9 @@ import ( "github.com/manifoldco/promptui" "github.com/urfave/cli/v2" + "github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/event" reg "github.com/photoprism/photoprism/internal/service/cluster/registry" "github.com/photoprism/photoprism/pkg/clean" ) @@ -112,6 +114,29 @@ func clusterNodesModAction(ctx *cli.Context) error { return cli.Exit(err, 1) } + nodeID := n.UUID + if nodeID == "" { + nodeID = n.Name + } + + changeSummary := strings.Join(changes, ", ") + + who := clusterAuditWho(ctx, conf) + segments := []string{ + string(acl.ResourceCluster), + "update node %s", + } + args := []interface{}{clean.Log(nodeID)} + + if changeSummary != "" { + segments = append(segments, "%s") + args = append(args, clean.Log(changeSummary)) + } + + segments = append(segments, event.Updated) + + event.AuditInfo(append(who, segments...), args...) + log.Infof("node %s has been updated", clean.LogQuote(n.Name)) return nil }) diff --git a/internal/commands/cluster_nodes_remove.go b/internal/commands/cluster_nodes_remove.go index 3f6217b4f..ea7e6f2a6 100644 --- a/internal/commands/cluster_nodes_remove.go +++ b/internal/commands/cluster_nodes_remove.go @@ -8,7 +8,9 @@ import ( "github.com/manifoldco/promptui" "github.com/urfave/cli/v2" + "github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/service/cluster/provisioner" reg "github.com/photoprism/photoprism/internal/service/cluster/registry" "github.com/photoprism/photoprism/pkg/clean" @@ -109,6 +111,13 @@ func clusterNodesRemoveAction(ctx *cli.Context) error { return cli.Exit(err, 1) } + who := clusterAuditWho(ctx, conf) + event.AuditInfo(append(who, + string(acl.ResourceCluster), + "node %s", + event.Deleted, + ), clean.Log(uuid)) + loggedDeletion := false if dropDB { @@ -122,6 +131,11 @@ func clusterNodesRemoveAction(ctx *cli.Context) error { return cli.Exit(fmt.Errorf("failed to drop database credentials for node %s: %w", clean.Log(uuid), err), 1) } log.Infof("node %s database %s and user %s have been dropped", clean.Log(uuid), clean.Log(dbName), clean.Log(dbUser)) + event.AuditInfo(append(who, + string(acl.ResourceCluster), + "drop database %s user %s", + event.Succeeded, + ), clean.Log(dbName), clean.Log(dbUser)) } } diff --git a/internal/commands/cluster_nodes_rotate.go b/internal/commands/cluster_nodes_rotate.go index 037269d66..00153a316 100644 --- a/internal/commands/cluster_nodes_rotate.go +++ b/internal/commands/cluster_nodes_rotate.go @@ -5,11 +5,14 @@ import ( "errors" "fmt" "os" + "strings" "github.com/manifoldco/promptui" "github.com/urfave/cli/v2" + "github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/service/cluster" reg "github.com/photoprism/photoprism/internal/service/cluster/registry" "github.com/photoprism/photoprism/internal/service/cluster/theme" @@ -170,6 +173,35 @@ func clusterNodesRotateAction(ctx *cli.Context) error { return cli.Exit(err, 1) } + nodeID := resp.Node.UUID + if nodeID == "" { + nodeID = resp.Node.Name + } + + rotatedParts := make([]string, 0, 2) + if rotateDatabase { + rotatedParts = append(rotatedParts, "database") + } + if rotateSecret { + rotatedParts = append(rotatedParts, "secret") + } + + detail := strings.Join(rotatedParts, ", ") + + who := clusterAuditWho(ctx, conf) + segments := []string{ + string(acl.ResourceCluster), + "rotate node %s", + } + args := []interface{}{clean.Log(nodeID)} + if detail != "" { + segments = append(segments, "%s") + args = append(args, clean.Log(detail)) + } + segments = append(segments, event.Succeeded) + + event.AuditInfo(append(who, segments...), args...) + if ctx.Bool("json") { jb, _ := json.Marshal(resp) fmt.Println(string(jb)) diff --git a/internal/commands/cluster_nodes_show.go b/internal/commands/cluster_nodes_show.go index 4cd4905a1..1626678ac 100644 --- a/internal/commands/cluster_nodes_show.go +++ b/internal/commands/cluster_nodes_show.go @@ -6,7 +6,9 @@ import ( "github.com/urfave/cli/v2" + "github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/event" reg "github.com/photoprism/photoprism/internal/service/cluster/registry" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/txt/report" @@ -57,6 +59,13 @@ func clusterNodesShowAction(ctx *cli.Context) error { opts := reg.NodeOpts{IncludeAdvertiseUrl: true, IncludeDatabase: true} dto := reg.BuildClusterNode(*n, opts) + who := clusterAuditWho(ctx, conf) + event.AuditInfo(append(who, + string(acl.ResourceCluster), + "show node %s", + event.Succeeded, + ), clean.Log(dto.UUID)) + if ctx.Bool("json") { b, _ := json.Marshal(dto) fmt.Println(string(b)) diff --git a/internal/commands/cluster_register.go b/internal/commands/cluster_register.go index aa341bc3f..814d725e0 100644 --- a/internal/commands/cluster_register.go +++ b/internal/commands/cluster_register.go @@ -16,7 +16,9 @@ import ( "github.com/urfave/cli/v2" + "github.com/photoprism/photoprism/internal/auth/acl" "github.com/photoprism/photoprism/internal/config" + "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/service/cluster" clusternode "github.com/photoprism/photoprism/internal/service/cluster/node" "github.com/photoprism/photoprism/internal/service/cluster/theme" @@ -293,6 +295,18 @@ func clusterRegisterAction(ctx *cli.Context) error { } } + nodeID := resp.Node.UUID + if nodeID == "" { + nodeID = resp.Node.Name + } + + who := clusterAuditWho(ctx, conf) + event.AuditInfo(append(who, + string(acl.ResourceCluster), + "register node %s", + event.Succeeded, + ), clean.Log(nodeID)) + // Optional persistence if ctx.Bool("write-config") { if err := persistRegisterResponse(conf, &resp); err != nil {