CLI: Add audit logs to cluster management commands

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-10-20 14:49:25 +02:00
parent 252aff2a6b
commit c5b5feee47
7 changed files with 127 additions and 0 deletions

View File

@@ -8,7 +8,11 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/urfave/cli/v2"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service/cluster" "github.com/photoprism/photoprism/internal/service/cluster"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/http/header" "github.com/photoprism/photoprism/pkg/http/header"
) )
@@ -53,3 +57,23 @@ func obtainClientCredentialsViaRegister(portalURL, joinToken, nodeName string) (
} }
return id, secret, nil 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}
}

View File

@@ -7,7 +7,9 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
reg "github.com/photoprism/photoprism/internal/service/cluster/registry" reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
"github.com/photoprism/photoprism/pkg/txt/report" "github.com/photoprism/photoprism/pkg/txt/report"
) )
@@ -74,6 +76,13 @@ func clusterNodesListAction(ctx *cli.Context) error {
opts := reg.NodeOpts{IncludeAdvertiseUrl: true, IncludeDatabase: true} opts := reg.NodeOpts{IncludeAdvertiseUrl: true, IncludeDatabase: true}
out := reg.BuildClusterNodes(page, opts) 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") { if ctx.Bool("json") {
b, _ := json.Marshal(out) b, _ := json.Marshal(out)
fmt.Println(string(b)) fmt.Println(string(b))

View File

@@ -7,7 +7,9 @@ import (
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
reg "github.com/photoprism/photoprism/internal/service/cluster/registry" reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
) )
@@ -112,6 +114,29 @@ func clusterNodesModAction(ctx *cli.Context) error {
return cli.Exit(err, 1) 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)) log.Infof("node %s has been updated", clean.LogQuote(n.Name))
return nil return nil
}) })

View File

@@ -8,7 +8,9 @@ import (
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service/cluster/provisioner" "github.com/photoprism/photoprism/internal/service/cluster/provisioner"
reg "github.com/photoprism/photoprism/internal/service/cluster/registry" reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
@@ -109,6 +111,13 @@ func clusterNodesRemoveAction(ctx *cli.Context) error {
return cli.Exit(err, 1) 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 loggedDeletion := false
if dropDB { 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) 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)) 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))
} }
} }

View File

@@ -5,11 +5,14 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service/cluster" "github.com/photoprism/photoprism/internal/service/cluster"
reg "github.com/photoprism/photoprism/internal/service/cluster/registry" reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
"github.com/photoprism/photoprism/internal/service/cluster/theme" "github.com/photoprism/photoprism/internal/service/cluster/theme"
@@ -170,6 +173,35 @@ func clusterNodesRotateAction(ctx *cli.Context) error {
return cli.Exit(err, 1) 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") { if ctx.Bool("json") {
jb, _ := json.Marshal(resp) jb, _ := json.Marshal(resp)
fmt.Println(string(jb)) fmt.Println(string(jb))

View File

@@ -6,7 +6,9 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
reg "github.com/photoprism/photoprism/internal/service/cluster/registry" reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
"github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt/report" "github.com/photoprism/photoprism/pkg/txt/report"
@@ -57,6 +59,13 @@ func clusterNodesShowAction(ctx *cli.Context) error {
opts := reg.NodeOpts{IncludeAdvertiseUrl: true, IncludeDatabase: true} opts := reg.NodeOpts{IncludeAdvertiseUrl: true, IncludeDatabase: true}
dto := reg.BuildClusterNode(*n, opts) 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") { if ctx.Bool("json") {
b, _ := json.Marshal(dto) b, _ := json.Marshal(dto)
fmt.Println(string(b)) fmt.Println(string(b))

View File

@@ -16,7 +16,9 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/photoprism/photoprism/internal/auth/acl"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/service/cluster" "github.com/photoprism/photoprism/internal/service/cluster"
clusternode "github.com/photoprism/photoprism/internal/service/cluster/node" clusternode "github.com/photoprism/photoprism/internal/service/cluster/node"
"github.com/photoprism/photoprism/internal/service/cluster/theme" "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 // Optional persistence
if ctx.Bool("write-config") { if ctx.Bool("write-config") {
if err := persistRegisterResponse(conf, &resp); err != nil { if err := persistRegisterResponse(conf, &resp); err != nil {