mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Logs: Add package pkg/log/status to provide generic outcome constants
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# PhotoPrism® Repository Guidelines
|
||||
|
||||
**Last Updated:** October 15, 2025
|
||||
**Last Updated:** October 21, 2025
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -335,6 +335,8 @@ Note: Across our public documentation, official images, and in production, the c
|
||||
- When introducing new metadata sources (e.g., `SrcOllama`, `SrcOpenAI`), define them in both `internal/entity/src.go` and the frontend lookup tables (`frontend/src/common/util.js`) so UI badges and server priorities stay aligned.
|
||||
- Vision worker scheduling is controlled via `VisionSchedule` / `VisionFilter` and the `Run` property set in `vision.yml`. Utilities like `vision.FilterModels` and `entity.Photo.ShouldGenerateLabels/Caption` help decide when work is required before loading media files.
|
||||
- Logging: use the shared logger (`event.Log`) via the package-level `log` variable (see `internal/auth/jwt/logger.go`) instead of direct `fmt.Print*` or ad-hoc loggers.
|
||||
- Audit outcomes: import `github.com/photoprism/photoprism/pkg/log/status` and end every `event.Audit*` slice with a single outcome token such as `status.Succeeded`, `status.Failed`, `status.Denied`, or other constants defined there (no additional segments afterwards).
|
||||
- Error outcomes: when a sanitized error string should be the outcome, call `status.Error(err)` instead of adding a placeholder and passing `clean.Error(err)` manually.
|
||||
- Cluster registry tests (`internal/service/cluster/registry`) currently rely on a full test config because they persist `entity.Client` rows. They run migrations and seed the SQLite DB, so they are intentionally slow. If you refactor them, consider sharing a single `config.TestConfig()` across subtests or building a lightweight schema harness; do not swap to the minimal config helper unless the tests stop touching the database.
|
||||
- Favor explicit CLI flags: check `c.cliCtx.IsSet("<flag>")` before overriding user-supplied values, and follow the `ClusterUUID` pattern (`options.yml` → CLI/env → generated UUIDv4 persisted).
|
||||
- Database helpers: reuse `conf.Db()` / `conf.Database*()`, avoid GORM `WithContext`, quote MySQL identifiers, and reject unsupported drivers early.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
PhotoPrism — Backend CODEMAP
|
||||
|
||||
**Last Updated:** October 14, 2025
|
||||
**Last Updated:** October 21, 2025
|
||||
|
||||
Purpose
|
||||
- Give agents and contributors a fast, reliable map of where things live and how they fit together, so you can add features, fix bugs, and write tests without spelunking.
|
||||
@@ -38,7 +38,7 @@ High-Level Package Map (Go)
|
||||
- `internal/workers` — background schedulers (index, vision, sync, meta, backup)
|
||||
- `internal/auth` — ACL, sessions, OIDC
|
||||
- `internal/service` — cluster/portal, maps, hub, webdav
|
||||
- `internal/event` — logging, pub/sub, audit
|
||||
- `internal/event` — logging, pub/sub, audit; canonical outcome tokens live in `pkg/log/status` (use helpers like `status.Error(err)` when the sanitized message should be the outcome)
|
||||
- `internal/ffmpeg`, `internal/thumb`, `internal/meta`, `internal/form`, `internal/mutex` — media, thumbs, metadata, forms, coordination
|
||||
- `pkg/*` — reusable utilities (must never import from `internal/*`), e.g. `pkg/clean`, `pkg/enum`, `pkg/fs`, `pkg/txt`, `pkg/http/header`
|
||||
|
||||
|
||||
@@ -96,6 +96,10 @@ We kindly ask you not to report bugs via GitHub Issues **unless you are certain
|
||||
- [Contact us](https://www.photoprism.app/contact) or [a community member](https://link.photoprism.app/discussions) if you need help, it could be a local configuration problem, or a misunderstanding in how the software works
|
||||
- This gives us the opportunity to [improve our documentation](https://docs.photoprism.app/getting-started/troubleshooting/) and provide best-in-class support instead of dealing with unclear/duplicate bug reports or triggering a flood of notifications by replying to comments
|
||||
|
||||
## Developer Notes ##
|
||||
|
||||
- When emitting audit logs, reuse the helpers in `pkg/log/status`; call `status.Error(err)` whenever the sanitized error message should serve as the outcome token. See `AGENTS.md` and `specs/common/audit-logs.md` for full guidelines.
|
||||
|
||||
## Connect with the Community ##
|
||||
|
||||
<a href="https://link.photoprism.app/chat"><img align="right" width="144" height="144" src="https://dl.photoprism.app/img/brands/element-logo.svg"></a>
|
||||
|
||||
@@ -34,7 +34,7 @@ The API package exposes PhotoPrism’s 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.
|
||||
- **Outcome:** End with a single token like `event.Succeeded`, `event.Failed`, or `authn.Denied`; nothing comes after it.
|
||||
- **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:
|
||||
```go
|
||||
@@ -43,7 +43,7 @@ The API package exposes PhotoPrism’s HTTP endpoints via Gin handlers. Each fil
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"node %s",
|
||||
event.Deleted,
|
||||
status.Deleted,
|
||||
}, s.RefID, uuid)
|
||||
|
||||
event.AuditErr([]string{
|
||||
@@ -51,9 +51,8 @@ The API package exposes PhotoPrism’s HTTP endpoints via Gin handlers. Each fil
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"download theme",
|
||||
"%s",
|
||||
event.Failed,
|
||||
}, refID, clean.Error(err))
|
||||
status.Error(err),
|
||||
}, refID)
|
||||
```
|
||||
- See `specs/common/audit-logs.md` for the full conventions and additional examples that agents should follow.
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ func CreateAlbum(router *gin.RouterGroup) {
|
||||
album := entity.NewUserAlbum(frm.AlbumTitle, entity.AlbumManual, conf.Settings().Albums.Order.Album, s.UserUID)
|
||||
album.AlbumFavorite = frm.AlbumFavorite
|
||||
|
||||
status := http.StatusOK
|
||||
code := http.StatusOK
|
||||
|
||||
// Existing album?
|
||||
if found := album.Find(); found == nil {
|
||||
@@ -154,7 +154,7 @@ func CreateAlbum(router *gin.RouterGroup) {
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
status = http.StatusCreated
|
||||
code = http.StatusCreated
|
||||
} else {
|
||||
// Exists, restore if necessary.
|
||||
album = found
|
||||
@@ -175,12 +175,12 @@ func CreateAlbum(router *gin.RouterGroup) {
|
||||
SaveAlbumYaml(*album)
|
||||
|
||||
// Add location header if newly created.
|
||||
if status == http.StatusCreated {
|
||||
if code == http.StatusCreated {
|
||||
header.SetLocation(c, c.FullPath(), album.AlbumUID)
|
||||
}
|
||||
|
||||
// Return as JSON.
|
||||
c.JSON(status, album)
|
||||
c.JSON(code, album)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
@@ -41,7 +42,7 @@ func SearchAlbums(router *gin.RouterGroup) {
|
||||
|
||||
// Abort if request params are invalid.
|
||||
if err = c.MustBindWith(&frm, binding.Form); err != nil {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", "albums", "search", "form invalid", "%s"}, s.RefID, err)
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", "albums", "search", "form invalid", status.Error(err)}, s.RefID)
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
@@ -58,7 +59,7 @@ func SearchAlbums(router *gin.RouterGroup) {
|
||||
|
||||
// Ok?
|
||||
if err != nil {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", "albums", "search", "%s"}, s.RefID, err)
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", "albums", "search", status.Error(err)}, s.RefID)
|
||||
c.AbortWithStatusJSON(400, gin.H{"error": txt.UpperFirst(err.Error())})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// Auth checks if the user is authorized to access a resource with the given permission
|
||||
@@ -37,7 +38,7 @@ func AuthAny(c *gin.Context, resource acl.Resource, perms acl.Permissions) (s *e
|
||||
if s = authAnyJWT(c, clientIp, authToken, resource, perms); s != nil {
|
||||
return s
|
||||
}
|
||||
event.AuditWarn([]string{clientIp, "%s %s without authentication", authn.Denied}, perms.String(), string(resource))
|
||||
event.AuditWarn([]string{clientIp, "%s %s without authentication", status.Denied}, perms.String(), string(resource))
|
||||
return entity.SessionStatusUnauthorized()
|
||||
}
|
||||
|
||||
@@ -55,25 +56,25 @@ func AuthAny(c *gin.Context, resource acl.Resource, perms acl.Permissions) (s *e
|
||||
|
||||
// Check request authorization against client application ACL rules.
|
||||
if acl.Rules.DenyAll(resource, s.GetClientRole(), perms) {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s", authn.Denied}, clean.Log(s.GetClientInfo()), s.RefID, perms.String(), string(resource))
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s", status.Denied}, clean.Log(s.GetClientInfo()), s.RefID, perms.String(), string(resource))
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
// Also check the request authorization against the user's ACL rules?
|
||||
if s.NoUser() {
|
||||
// Allow access based on the ACL defaults for client applications.
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s", authn.Granted}, clean.Log(s.GetClientInfo()), s.RefID, perms.String(), string(resource))
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s", status.Granted}, clean.Log(s.GetClientInfo()), s.RefID, perms.String(), string(resource))
|
||||
} else if u := s.GetUser(); !u.IsDisabled() && !u.IsUnknown() && u.IsRegistered() {
|
||||
if acl.Rules.DenyAll(resource, u.AclRole(), perms) {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as %s", authn.Denied}, clean.Log(s.GetClientInfo()), s.RefID, perms.String(), string(resource), u.String())
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as %s", status.Denied}, clean.Log(s.GetClientInfo()), s.RefID, perms.String(), string(resource), u.String())
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
// Allow access based on the user role.
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s as %s", authn.Granted}, clean.Log(s.GetClientInfo()), s.RefID, perms.String(), string(resource), u.String())
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s as %s", status.Granted}, clean.Log(s.GetClientInfo()), s.RefID, perms.String(), string(resource), u.String())
|
||||
} else {
|
||||
// Deny access if it is not a regular user account or the account has been disabled.
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as unauthorized user", authn.Denied}, clean.Log(s.GetClientInfo()), s.RefID, perms.String(), string(resource))
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as unauthorized user", status.Denied}, clean.Log(s.GetClientInfo()), s.RefID, perms.String(), string(resource))
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
@@ -82,13 +83,13 @@ func AuthAny(c *gin.Context, resource acl.Resource, perms acl.Permissions) (s *e
|
||||
|
||||
// Otherwise, perform a regular ACL authorization check based on the user role.
|
||||
if u := s.GetUser(); u.IsUnknown() || u.IsDisabled() {
|
||||
event.AuditWarn([]string{clientIp, "session %s", "%s %s as unauthorized user", authn.Denied}, s.RefID, perms.String(), string(resource))
|
||||
event.AuditWarn([]string{clientIp, "session %s", "%s %s as unauthorized user", status.Denied}, s.RefID, perms.String(), string(resource))
|
||||
return entity.SessionStatusUnauthorized()
|
||||
} else if acl.Rules.DenyAll(resource, u.AclRole(), perms) {
|
||||
event.AuditErr([]string{clientIp, "session %s", "%s %s as %s", authn.Denied}, s.RefID, perms.String(), string(resource), u.AclRole().String())
|
||||
event.AuditErr([]string{clientIp, "session %s", "%s %s as %s", status.Denied}, s.RefID, perms.String(), string(resource), u.AclRole().String())
|
||||
return entity.SessionStatusForbidden()
|
||||
} else {
|
||||
event.AuditInfo([]string{clientIp, "session %s", "%s %s as %s", authn.Granted}, s.RefID, perms.String(), string(resource), u.AclRole().String())
|
||||
event.AuditInfo([]string{clientIp, "session %s", "%s %s as %s", status.Granted}, s.RefID, perms.String(), string(resource), u.AclRole().String())
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity/search"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// Event represents an api event type.
|
||||
@@ -28,7 +29,7 @@ func (ev Event) String() string {
|
||||
// PublishPhotoEvent publishes updated photo data after changes have been made.
|
||||
func PublishPhotoEvent(ev Event, uid string, c *gin.Context) {
|
||||
if result, _, err := search.Photos(form.SearchPhotos{UID: uid, Merged: true}); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s photo %s", "%s"}, AuthToken(c), string(ev), uid, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s photo %s", status.Error(err)}, AuthToken(c), string(ev), uid)
|
||||
} else {
|
||||
event.PublishEntities("photos", string(ev), result)
|
||||
}
|
||||
@@ -38,7 +39,7 @@ func PublishPhotoEvent(ev Event, uid string, c *gin.Context) {
|
||||
func PublishAlbumEvent(ev Event, uid string, c *gin.Context) {
|
||||
f := form.SearchAlbums{UID: uid}
|
||||
if result, err := search.Albums(f); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s album %s", "%s"}, AuthToken(c), string(ev), uid, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s album %s", status.Error(err)}, AuthToken(c), string(ev), uid)
|
||||
} else {
|
||||
event.PublishEntities("albums", string(ev), result)
|
||||
}
|
||||
@@ -48,7 +49,7 @@ func PublishAlbumEvent(ev Event, uid string, c *gin.Context) {
|
||||
func PublishLabelEvent(ev Event, uid string, c *gin.Context) {
|
||||
f := form.SearchLabels{UID: uid}
|
||||
if result, err := search.Labels(f); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s label %s", "%s"}, AuthToken(c), string(ev), uid, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s label %s", status.Error(err)}, AuthToken(c), string(ev), uid)
|
||||
} else {
|
||||
event.PublishEntities("labels", string(ev), result)
|
||||
}
|
||||
@@ -58,7 +59,7 @@ func PublishLabelEvent(ev Event, uid string, c *gin.Context) {
|
||||
func PublishSubjectEvent(ev Event, uid string, c *gin.Context) {
|
||||
f := form.SearchSubjects{UID: uid}
|
||||
if result, err := search.Subjects(f); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s subject %s", "%s"}, AuthToken(c), string(ev), uid, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "%s subject %s", status.Error(err)}, AuthToken(c), string(ev), uid)
|
||||
} else {
|
||||
event.PublishEntities("subjects", string(ev), result)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/service/cluster"
|
||||
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
@@ -108,14 +109,13 @@ func ClusterListNodes(router *gin.RouterGroup) {
|
||||
resp := reg.BuildClusterNodes(page, opts)
|
||||
|
||||
// Audit list access.
|
||||
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))
|
||||
event.AuditDebug(
|
||||
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "list nodes", "count %d offset %d returned %d", status.Succeeded},
|
||||
s.RefID,
|
||||
count,
|
||||
offset,
|
||||
len(resp),
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
})
|
||||
@@ -173,13 +173,11 @@ func ClusterGetNode(router *gin.RouterGroup) {
|
||||
resp := reg.BuildClusterNode(*n, opts)
|
||||
|
||||
// Audit get access.
|
||||
event.AuditInfo([]string{
|
||||
ClientIP(c),
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"get node %s",
|
||||
event.Succeeded,
|
||||
}, s.RefID, uuid)
|
||||
event.AuditInfo(
|
||||
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "get node %s", status.Succeeded},
|
||||
s.RefID,
|
||||
uuid,
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
})
|
||||
@@ -263,13 +261,11 @@ func ClusterUpdateNode(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditInfo([]string{
|
||||
ClientIP(c),
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"node %s",
|
||||
event.Updated,
|
||||
}, s.RefID, uuid)
|
||||
event.AuditInfo(
|
||||
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "node %s", status.Updated},
|
||||
s.RefID,
|
||||
uuid,
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, cluster.StatusResponse{Status: "ok"})
|
||||
})
|
||||
@@ -324,13 +320,11 @@ func ClusterDeleteNode(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditWarn([]string{
|
||||
ClientIP(c),
|
||||
"session %s",
|
||||
string(acl.ResourceCluster),
|
||||
"node %s",
|
||||
event.Deleted,
|
||||
}, s.RefID, uuid)
|
||||
event.AuditWarn(
|
||||
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "node %s", status.Deleted},
|
||||
s.RefID,
|
||||
uuid,
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, cluster.StatusResponse{Status: "ok"})
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/service/cluster/theme"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
@@ -53,7 +54,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", "rate limit exceeded", event.Denied})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "rate limit exceeded", status.Denied})
|
||||
limiter.AbortJSON(c)
|
||||
return
|
||||
}
|
||||
@@ -63,7 +64,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", "invalid join token", event.Denied})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid join token", status.Denied})
|
||||
r.Success() // return reserved tokens; still unauthorized
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
@@ -73,7 +74,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", "invalid form", "%s", event.Failed}, clean.Error(err))
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid form", status.Error(err)})
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
@@ -85,13 +86,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", "invalid client id", event.Failed})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid client id", status.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", "invalid client secret", event.Denied})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid client secret", status.Denied})
|
||||
AbortUnauthorized(c)
|
||||
return
|
||||
}
|
||||
@@ -101,14 +102,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", "invalid name", event.Failed})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid name", status.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", "invalid name chars", event.Failed})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid name chars", status.Failed})
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
@@ -117,7 +118,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", "invalid advertise url", event.Failed})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid advertise url", status.Failed})
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
@@ -126,7 +127,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", "invalid site url", event.Failed})
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "invalid site url", status.Failed})
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
@@ -139,7 +140,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
regy, err := reg.NewClientRegistryWithConfig(conf)
|
||||
|
||||
if err != nil {
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "register", "%s", event.Failed}, clean.Error(err))
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "register", status.Error(err)})
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
@@ -154,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", event.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
|
||||
}
|
||||
@@ -186,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", event.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", event.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", "%s", event.Failed}, clean.Log(name), clean.Error(putErr))
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node %s", "persist node", status.Error(putErr)}, clean.Log(name))
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
@@ -205,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", "%s", event.Failed}, clean.Log(name), clean.Error(err))
|
||||
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", event.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", "%s", event.Failed}, clean.Log(name), clean.Error(putErr))
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "node %s", "persist rotated secret", status.Error(putErr)}, clean.Log(name))
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
@@ -229,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", "%s", event.Failed}, clean.Log(name), clean.Error(credsErr))
|
||||
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
|
||||
}
|
||||
@@ -239,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", "%s", event.Failed}, clean.Log(name), clean.Error(putErr))
|
||||
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", event.Succeeded}, clean.Log(name))
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", "rotate database", status.Succeeded}, clean.Log(name))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,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", event.Updated}, clean.Log(name))
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", status.Synced}, clean.Log(name))
|
||||
c.JSON(http.StatusOK, resp)
|
||||
return
|
||||
}
|
||||
@@ -319,7 +320,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", "ensure database", "%s", event.Failed}, clean.Error(err))
|
||||
event.AuditWarn([]string{clientIp, string(acl.ResourceCluster), "register", "ensure database", status.Error(err)})
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -333,7 +334,7 @@ func ClusterNodesRegister(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
if err = regy.Put(n); err != nil {
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "register", "persist node", "%s", event.Failed}, clean.Error(err))
|
||||
event.AuditErr([]string{clientIp, string(acl.ResourceCluster), "register", "persist node", status.Error(err)})
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
}
|
||||
@@ -359,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", event.Joined}, clean.Log(name))
|
||||
event.AuditInfo([]string{clientIp, string(acl.ResourceCluster), "node %s", status.Joined}, clean.Log(name))
|
||||
c.JSON(http.StatusCreated, resp)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
|
||||
"github.com/photoprism/photoprism/internal/service/cluster/theme"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// ClusterSummary returns a minimal overview of the cluster/portal.
|
||||
@@ -61,13 +62,11 @@ func ClusterSummary(router *gin.RouterGroup) {
|
||||
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())
|
||||
event.AuditDebug(
|
||||
[]string{ClientIP(c), "session %s", string(acl.ResourceCluster), "get summary for cluster uuid %s", status.Succeeded},
|
||||
s.RefID,
|
||||
conf.ClusterUUID(),
|
||||
)
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
})
|
||||
@@ -96,12 +95,7 @@ func ClusterHealth(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditDebug([]string{
|
||||
ClientIP(c),
|
||||
string(acl.ResourceCluster),
|
||||
"health check",
|
||||
event.Succeeded,
|
||||
})
|
||||
event.AuditDebug([]string{ClientIP(c), string(acl.ResourceCluster), "health check", status.Succeeded})
|
||||
|
||||
c.JSON(http.StatusOK, NewHealthResponse("ok"))
|
||||
})
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// ClusterGetTheme returns custom theme files as zip, if available.
|
||||
@@ -75,7 +76,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), "download theme resolve", "%s", event.Failed}, refID, clean.Error(err))
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme resolve", status.Error(err)}, refID)
|
||||
AbortNotFound(c)
|
||||
return
|
||||
} else {
|
||||
@@ -84,7 +85,7 @@ func ClusterGetTheme(router *gin.RouterGroup) {
|
||||
|
||||
// Check if theme path exists.
|
||||
if !fs.PathExists(themePath) {
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme path", "not found"}, refID)
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme path", status.NotFound}, refID)
|
||||
AbortNotFound(c)
|
||||
return
|
||||
}
|
||||
@@ -103,7 +104,7 @@ func ClusterGetTheme(router *gin.RouterGroup) {
|
||||
if version, err := theme.DetectVersion(themePath); err == nil {
|
||||
updateNodeThemeVersion(conf, session, version, clientIp, refID)
|
||||
} else {
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "detect theme version", "%s", event.Failed}, refID, clean.Error(err))
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "detect theme version", "%s", status.Failed}, refID, clean.Error(err))
|
||||
}
|
||||
|
||||
// Add response headers.
|
||||
@@ -114,14 +115,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), "download theme close", "%s", event.Failed}, refID, clean.Error(closeErr))
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme close", status.Error(closeErr)}, refID)
|
||||
}
|
||||
}(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), "download theme traverse", "%s", event.Failed}, refID, clean.Error(walkErr))
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme traverse", status.Error(walkErr)}, refID)
|
||||
|
||||
// If the error occurs on a directory, skip descending to avoid cascading errors.
|
||||
if info != nil && info.IsDir() {
|
||||
@@ -157,11 +158,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), "download theme add", "%s", event.Added}, refID, clean.Log(alias))
|
||||
event.AuditDebug([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme add", "%s", status.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), "download theme add %s", "%s", event.Failed}, refID, clean.Log(alias), clean.Error(zipErr))
|
||||
event.AuditWarn([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme add %s", status.Error(zipErr)}, refID, clean.Log(alias))
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -169,9 +170,9 @@ func ClusterGetTheme(router *gin.RouterGroup) {
|
||||
|
||||
// Log result.
|
||||
if err != nil {
|
||||
event.AuditErr([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme", "%s", event.Failed}, refID, clean.Error(err))
|
||||
event.AuditErr([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme", status.Error(err)}, refID)
|
||||
} else {
|
||||
event.AuditInfo([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme", event.Succeeded}, refID)
|
||||
event.AuditInfo([]string{clientIp, "session %s", string(acl.ResourceCluster), "download theme", status.Succeeded}, refID)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -198,7 +199,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", event.Failed}, refID, clean.Error(err))
|
||||
event.AuditDebug([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme metadata registry", "%s", status.Failed}, refID, clean.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -228,9 +229,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", event.Failed}, refID, clean.Error(err))
|
||||
event.AuditWarn([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme metadata", status.Error(err)}, refID)
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditDebug([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme metadata", event.Updated}, refID)
|
||||
event.AuditDebug([]string{clientIP, "session %s", string(acl.ResourceCluster), "theme metadata", status.Updated}, refID)
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
@@ -13,19 +13,19 @@ import (
|
||||
// for download until the cache expires, or the server is restarted.
|
||||
func Register(fileUuid, fileName string) error {
|
||||
if !rnd.IsUUID(fileUuid) {
|
||||
event.AuditWarn([]string{"api", "download", "create temporary token for %s", authn.Failed}, fileName)
|
||||
event.AuditWarn([]string{"api", "download", "create temporary token for %s", status.Failed}, fileName)
|
||||
return errors.New("invalid file uuid")
|
||||
}
|
||||
|
||||
if fileName = fs.Abs(fileName); !fs.FileExists(fileName) {
|
||||
event.AuditWarn([]string{"api", "download", "create temporary token for %s", authn.Failed}, fileName)
|
||||
event.AuditWarn([]string{"api", "download", "create temporary token for %s", status.Failed}, fileName)
|
||||
return errors.New("file not found")
|
||||
} else if Deny(fileName) {
|
||||
event.AuditErr([]string{"api", "download", "create temporary token for %s", authn.Denied}, fileName)
|
||||
event.AuditErr([]string{"api", "download", "create temporary token for %s", status.Denied}, fileName)
|
||||
return errors.New("forbidden file path")
|
||||
}
|
||||
|
||||
event.AuditInfo([]string{"api", "download", "create temporary token for %s", authn.Succeeded}, fileName)
|
||||
event.AuditInfo([]string{"api", "download", "create temporary token for %s", status.Succeeded}, fileName)
|
||||
|
||||
cache.SetDefault(fileUuid, fileName)
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
@@ -86,9 +86,9 @@ func StartImport(router *gin.RouterGroup) {
|
||||
// To avoid conflicts, uploads are imported from "import_path/upload/session_ref/timestamp".
|
||||
if token := path.Base(srcFolder); token != "" && path.Dir(srcFolder) == UploadPath {
|
||||
srcFolder = path.Join(UploadPath, s.RefID+token)
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", "import uploads from %s as %s", authn.Granted}, s.RefID, clean.Log(srcFolder), s.GetUserRole().String())
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", "import uploads from %s as %s", status.Granted}, s.RefID, clean.Log(srcFolder), s.GetUserRole().String())
|
||||
} else if acl.Rules.Deny(acl.ResourceFiles, s.GetUserRole(), acl.ActionManage) {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "import files from %s as %s", authn.Denied}, s.RefID, clean.Log(srcFolder), s.GetUserRole().String())
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "import files from %s as %s", status.Denied}, s.RefID, clean.Log(srcFolder), s.GetUserRole().String())
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
@@ -81,7 +82,7 @@ func OAuthRevoke(router *gin.RouterGroup) {
|
||||
|
||||
// Get the auth token to be revoked from the submitted form values or the request header.
|
||||
if err = c.ShouldBind(&frm); err != nil && authToken == "" {
|
||||
event.AuditWarn([]string{clientIp, "oauth2", actor, action, "%s"}, err)
|
||||
event.AuditWarn([]string{clientIp, "oauth2", actor, action, status.Error(err)})
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
} else if frm.Empty() {
|
||||
@@ -91,7 +92,7 @@ func OAuthRevoke(router *gin.RouterGroup) {
|
||||
|
||||
// Validate revocation form values.
|
||||
if err = frm.Validate(); err != nil {
|
||||
event.AuditWarn([]string{clientIp, "oauth2", actor, action, "%s"}, err)
|
||||
event.AuditWarn([]string{clientIp, "oauth2", actor, action, status.Error(err)})
|
||||
AbortInvalidCredentials(c)
|
||||
return
|
||||
}
|
||||
@@ -133,18 +134,18 @@ func OAuthRevoke(router *gin.RouterGroup) {
|
||||
|
||||
// Check revocation request and abort if invalid.
|
||||
if err != nil {
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "delete %s as %s", "%s"}, clean.Log(sess.RefID), role.String(), err.Error())
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "delete %s as %s", status.Error(err)}, clean.Log(sess.RefID), role.String())
|
||||
AbortInvalidCredentials(c)
|
||||
return
|
||||
} else if sess == nil {
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "delete %s as %s", authn.Denied}, clean.Log(sess.RefID), role.String())
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "delete %s as %s", status.Denied}, clean.Log(sess.RefID), role.String())
|
||||
AbortInvalidCredentials(c)
|
||||
return
|
||||
} else if sess.Abort(c) {
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "delete %s as %s", authn.Denied}, clean.Log(sess.RefID), role.String())
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "delete %s as %s", status.Denied}, clean.Log(sess.RefID), role.String())
|
||||
return
|
||||
} else if !sess.IsClient() {
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "delete %s as %s", authn.Denied}, clean.Log(sess.RefID), role.String())
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "delete %s as %s", status.Denied}, clean.Log(sess.RefID), role.String())
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, i18n.NewResponse(http.StatusForbidden, i18n.ErrForbidden))
|
||||
return
|
||||
} else if sUserUID != "" && sess.UserUID != sUserUID {
|
||||
@@ -152,13 +153,13 @@ func OAuthRevoke(router *gin.RouterGroup) {
|
||||
AbortInvalidCredentials(c)
|
||||
return
|
||||
} else {
|
||||
event.AuditInfo([]string{clientIp, "oauth2", actor, action, "delete %s as %s", authn.Granted}, clean.Log(sess.RefID), role.String())
|
||||
event.AuditInfo([]string{clientIp, "oauth2", actor, action, "delete %s as %s", status.Granted}, clean.Log(sess.RefID), role.String())
|
||||
}
|
||||
|
||||
// Delete session cache and database record.
|
||||
if err = sess.Delete(); err != nil {
|
||||
// Log error.
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "delete %s as %s", "%s"}, clean.Log(sess.RefID), role.String(), err)
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "delete %s as %s", status.Error(err)}, clean.Log(sess.RefID), role.String())
|
||||
|
||||
// Return JSON error.
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, i18n.NewResponse(http.StatusNotFound, i18n.ErrNotFound))
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/dustin/go-humanize/english"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// OAuthToken creates a new access token for clients using OAuth2 grant types.
|
||||
@@ -65,14 +65,14 @@ func OAuthToken(router *gin.RouterGroup) {
|
||||
frm.ClientID = clientId
|
||||
frm.ClientSecret = clientSecret
|
||||
} else if err = c.ShouldBind(&frm); err != nil {
|
||||
event.AuditWarn([]string{clientIp, "oauth2", actor, action, "%s"}, err)
|
||||
event.AuditWarn([]string{clientIp, "oauth2", actor, action, status.Error(err)})
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check the credentials for completeness and the correct format.
|
||||
if err = frm.Validate(); err != nil {
|
||||
event.AuditWarn([]string{clientIp, "oauth2", actor, action, "%s"}, err)
|
||||
event.AuditWarn([]string{clientIp, "oauth2", actor, action, status.Error(err)})
|
||||
AbortInvalidCredentials(c)
|
||||
return
|
||||
}
|
||||
@@ -151,13 +151,13 @@ func OAuthToken(router *gin.RouterGroup) {
|
||||
authUser, authProvider, authMethod, authErr := entity.Auth(loginForm, nil, c)
|
||||
|
||||
if authProvider.IsClient() {
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, authn.Denied})
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, status.Denied})
|
||||
AbortInvalidCredentials(c)
|
||||
return
|
||||
} else if authMethod.Is(authn.Method2FA) && errors.Is(authErr, authn.ErrPasscodeRequired) {
|
||||
// Ok.
|
||||
} else if authErr != nil {
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, "%s"}, strings.ToLower(clean.Error(authErr)))
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, status.Error(authErr)})
|
||||
AbortInvalidCredentials(c)
|
||||
return
|
||||
} else if !authUser.Equal(s.GetUser()) {
|
||||
@@ -183,7 +183,7 @@ func OAuthToken(router *gin.RouterGroup) {
|
||||
|
||||
// Save new session.
|
||||
if sess, err = get.Session().Save(sess); err != nil {
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, err.Error()})
|
||||
event.AuditErr([]string{clientIp, "oauth2", actor, action, status.Error(err)})
|
||||
AbortInvalidCredentials(c)
|
||||
return
|
||||
} else if sess == nil {
|
||||
@@ -191,7 +191,7 @@ func OAuthToken(router *gin.RouterGroup) {
|
||||
AbortUnexpectedError(c)
|
||||
return
|
||||
} else {
|
||||
event.AuditInfo([]string{clientIp, "oauth2", actor, action, authn.Created})
|
||||
event.AuditInfo([]string{clientIp, "oauth2", actor, action, status.Created})
|
||||
}
|
||||
|
||||
// Delete any existing client sessions above the configured limit.
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/time/tz"
|
||||
"github.com/photoprism/photoprism/pkg/time/unix"
|
||||
@@ -346,11 +347,11 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
|
||||
// Save session after successful authentication.
|
||||
if sess, err = get.Session().Save(sess); err != nil {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, "%s"}, err)
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, status.Error(err)})
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||
return
|
||||
} else if sess == nil {
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, authn.Failed})
|
||||
event.AuditErr([]string{clientIp, "create session", "oidc", userName, status.Failed})
|
||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrUnexpected)))
|
||||
return
|
||||
}
|
||||
@@ -362,7 +363,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
||||
response := CreateSessionResponse(sess.AuthToken(), sess, conf.ClientSession(sess))
|
||||
|
||||
// Log session created event.
|
||||
event.AuditInfo([]string{clientIp, "session %s", "oidc", userName, authn.Created}, sess.RefID)
|
||||
event.AuditInfo([]string{clientIp, "session %s", "oidc", userName, status.Created}, sess.RefID)
|
||||
|
||||
// Log session expiration time.
|
||||
if expires := sess.ExpiresAt(); !expires.IsZero() {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// SearchPhotos finds pictures and returns them as JSON.
|
||||
@@ -48,7 +49,7 @@ func SearchPhotos(router *gin.RouterGroup) {
|
||||
|
||||
// Abort if request params are invalid.
|
||||
if err = c.MustBindWith(&frm, binding.Form); err != nil {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePhotos), "form invalid", "%s"}, s.RefID, err)
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePhotos), "form invalid", status.Error(err)}, s.RefID)
|
||||
AbortBadRequest(c, err)
|
||||
return frm, s, err
|
||||
}
|
||||
@@ -84,7 +85,7 @@ func SearchPhotos(router *gin.RouterGroup) {
|
||||
|
||||
// Ok?
|
||||
if err != nil {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePhotos), "search", "%s"}, s.RefID, err)
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePhotos), "search", status.Error(err)}, s.RefID)
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
@@ -113,7 +114,7 @@ func SearchPhotos(router *gin.RouterGroup) {
|
||||
result, count, err := search.UserPhotosViewerResults(f, s, conf.ContentUri(), conf.ApiUri(), s.PreviewToken, s.DownloadToken)
|
||||
|
||||
if err != nil {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePhotos), "view", "%s"}, s.RefID, err)
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePhotos), "view", status.Error(err)}, s.RefID)
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
@@ -48,7 +49,7 @@ func SearchGeo(router *gin.RouterGroup) {
|
||||
|
||||
// Abort if request params are invalid.
|
||||
if err = c.MustBindWith(&frm, binding.Form); err != nil {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePlaces), "form invalid", "%s"}, s.RefID, err)
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePlaces), "form invalid", status.Error(err)}, s.RefID)
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
@@ -73,7 +74,7 @@ func SearchGeo(router *gin.RouterGroup) {
|
||||
|
||||
// Ok?
|
||||
if err != nil {
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePlaces), "search", "%s"}, s.RefID, err)
|
||||
event.AuditWarn([]string{ClientIP(c), "session %s", string(acl.ResourcePlaces), "search", status.Error(err)}, s.RefID)
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// CreateSession creates a new client session (login) and returns session data.
|
||||
@@ -41,7 +42,7 @@ func CreateSession(router *gin.RouterGroup) {
|
||||
|
||||
// Assign and validate request form values.
|
||||
if err := c.BindJSON(&frm); err != nil {
|
||||
event.AuditWarn([]string{clientIp, "create session", "invalid request", "%s"}, err)
|
||||
event.AuditWarn([]string{clientIp, "create session", "invalid request", status.Error(err)})
|
||||
AbortBadRequest(c, err)
|
||||
return
|
||||
}
|
||||
@@ -119,7 +120,7 @@ func CreateSession(router *gin.RouterGroup) {
|
||||
|
||||
// Save session after successful authentication.
|
||||
if sess, err = get.Session().Save(sess); err != nil {
|
||||
event.AuditErr([]string{clientIp, "%s"}, err)
|
||||
event.AuditErr([]string{clientIp, status.Error(err)})
|
||||
c.AbortWithStatusJSON(sess.HttpStatus(), gin.H{"error": i18n.Msg(i18n.ErrInvalidCredentials)})
|
||||
return
|
||||
} else if sess == nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -12,7 +13,6 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/photoprism/get"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
@@ -58,12 +58,12 @@ func DeleteSession(router *gin.RouterGroup) {
|
||||
// Only admins may delete other sessions by ref id.
|
||||
if rnd.IsRefID(id) {
|
||||
if !acl.Rules.AllowAll(acl.ResourceSessions, s.GetUserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
event.AuditErr([]string{clientIp, "session %s", "delete %s as %s", authn.Denied}, s.RefID, acl.ResourceSessions.String(), s.GetUserRole())
|
||||
event.AuditErr([]string{clientIp, "session %s", "delete %s as %s", status.Denied}, s.RefID, acl.ResourceSessions.String(), s.GetUserRole())
|
||||
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditInfo([]string{clientIp, "session %s", "delete %s as %s", authn.Granted}, s.RefID, acl.ResourceSessions.String(), s.GetUserRole())
|
||||
event.AuditInfo([]string{clientIp, "session %s", "delete %s as %s", status.Granted}, s.RefID, acl.ResourceSessions.String(), s.GetUserRole())
|
||||
|
||||
if s = entity.FindSessionByRefID(id); s == nil {
|
||||
Abort(c, http.StatusNotFound, i18n.ErrNotFound)
|
||||
@@ -77,7 +77,7 @@ func DeleteSession(router *gin.RouterGroup) {
|
||||
|
||||
// Delete session cache and database record.
|
||||
if err := s.Delete(); err != nil {
|
||||
event.AuditErr([]string{clientIp, "session %s", "delete session as %s", "%s"}, s.RefID, s.GetUserRole(), err)
|
||||
event.AuditErr([]string{clientIp, "session %s", "delete session as %s", status.Error(err)}, s.RefID, s.GetUserRole())
|
||||
} else {
|
||||
event.AuditDebug([]string{clientIp, "session %s", "deleted"}, s.RefID)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// UploadUserAvatar updates the avatar image of the specified user.
|
||||
@@ -60,7 +61,7 @@ func UploadUserAvatar(router *gin.RouterGroup) {
|
||||
f, err := c.MultipartForm()
|
||||
|
||||
if err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", status.Error(err)}, s.RefID)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
}
|
||||
@@ -85,7 +86,7 @@ func UploadUserAvatar(router *gin.RouterGroup) {
|
||||
uploadDir, err := conf.UserUploadPath(uid, "")
|
||||
|
||||
if err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "failed to create folder", "%s"}, s.RefID, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "failed to create folder", status.Error(err)}, s.RefID)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
}
|
||||
@@ -99,11 +100,11 @@ func UploadUserAvatar(router *gin.RouterGroup) {
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrFileTooLarge)
|
||||
return
|
||||
} else if fReader, fErr := file.Open(); fErr != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", status.Error(fErr)}, s.RefID)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
} else if mimeType, mimeErr := mimetype.DetectReader(fReader); mimeErr != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", status.Error(mimeErr)}, s.RefID)
|
||||
Abort(c, http.StatusBadRequest, i18n.ErrUploadFailed)
|
||||
return
|
||||
} else {
|
||||
@@ -133,7 +134,7 @@ func UploadUserAvatar(router *gin.RouterGroup) {
|
||||
|
||||
// Set user avatar image.
|
||||
if err = avatar.SetUserImage(m, filePath, entity.SrcManual, conf.ThumbCachePath()); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", "%s"}, s.RefID, err)
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "upload avatar", status.Error(err)}, s.RefID)
|
||||
}
|
||||
|
||||
// Clear session cache to update user details.
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
@@ -82,7 +83,7 @@ func CreateUserPasscode(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, authn.Created}, s.RefID)
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, status.Created}, s.RefID)
|
||||
|
||||
header.SetLocation(c)
|
||||
c.JSON(http.StatusCreated, passcode)
|
||||
@@ -134,7 +135,7 @@ func ConfirmUserPasscode(router *gin.RouterGroup) {
|
||||
// Return the reserved request rate limit tokens after successful authentication.
|
||||
r.Success()
|
||||
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, authn.Verified}, s.RefID)
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, status.Verified}, s.RefID)
|
||||
|
||||
// Clear session cache.
|
||||
s.ClearCache()
|
||||
@@ -172,7 +173,7 @@ func ActivateUserPasscode(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
// Log event.
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, authn.Activated}, s.RefID)
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, status.Activated}, s.RefID)
|
||||
|
||||
// Invalidate any other user sessions to protect the account:
|
||||
// https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
|
||||
@@ -235,7 +236,7 @@ func DeactivateUserPasscode(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, authn.Deactivated}, s.RefID)
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", authn.Users, user.UserName, authn.Passcode, status.Deactivated}, s.RefID)
|
||||
|
||||
// Clear session cache.
|
||||
s.ClearCache()
|
||||
|
||||
@@ -276,38 +276,38 @@ func VerifyTokenWithKeys(tokenString string, expected ExpectedClaims, keys []Pub
|
||||
|
||||
// Status returns diagnostic information about the verifier's current JWKS cache.
|
||||
func (v *Verifier) Status(ttl time.Duration) VerifierStatus {
|
||||
status := VerifierStatus{}
|
||||
result := VerifierStatus{}
|
||||
|
||||
if ttl > 0 {
|
||||
status.CacheTTLSeconds = int(ttl / time.Second)
|
||||
result.CacheTTLSeconds = int(ttl / time.Second)
|
||||
}
|
||||
|
||||
v.mu.Lock()
|
||||
defer v.mu.Unlock()
|
||||
|
||||
status.CacheURL = v.cache.URL
|
||||
status.CacheETag = v.cache.ETag
|
||||
status.JWKSURL = v.cache.URL
|
||||
status.KeyCount = len(v.cache.Keys)
|
||||
status.KeyIDs = make([]string, 0, len(v.cache.Keys))
|
||||
result.CacheURL = v.cache.URL
|
||||
result.CacheETag = v.cache.ETag
|
||||
result.JWKSURL = v.cache.URL
|
||||
result.KeyCount = len(v.cache.Keys)
|
||||
result.KeyIDs = make([]string, 0, len(v.cache.Keys))
|
||||
|
||||
for _, key := range v.cache.Keys {
|
||||
status.KeyIDs = append(status.KeyIDs, key.Kid)
|
||||
result.KeyIDs = append(result.KeyIDs, key.Kid)
|
||||
}
|
||||
|
||||
status.CachePath = v.cachePath
|
||||
result.CachePath = v.cachePath
|
||||
|
||||
if v.cache.FetchedAt > 0 {
|
||||
fetched := time.Unix(v.cache.FetchedAt, 0).UTC()
|
||||
status.CacheFetchedAt = fetched
|
||||
result.CacheFetchedAt = fetched
|
||||
age := time.Since(fetched)
|
||||
status.CacheAgeSeconds = int64(age.Seconds())
|
||||
result.CacheAgeSeconds = int64(age.Seconds())
|
||||
if ttl > 0 && age > ttl {
|
||||
status.CacheStale = true
|
||||
result.CacheStale = true
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
return result
|
||||
}
|
||||
|
||||
// publicKeyForKid resolves the public key for the given key ID, fetching JWKS data if needed.
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
@@ -32,11 +33,11 @@ type Client struct {
|
||||
func NewClient(issuerUri *url.URL, oidcClient, oidcSecret, oidcScopes, siteUrl string, insecure bool) (result *Client, err error) {
|
||||
if issuerUri == nil {
|
||||
err = errors.New("issuer uri required")
|
||||
event.AuditErr([]string{"oidc", "provider", "%s"}, err)
|
||||
event.AuditErr([]string{"oidc", "provider", status.Error(err)})
|
||||
return nil, errors.New("issuer uri required")
|
||||
} else if insecure == false && issuerUri.Scheme != "https" {
|
||||
err = errors.New("issuer uri must use https")
|
||||
event.AuditErr([]string{"oidc", "provider", "%s"}, err)
|
||||
event.AuditErr([]string{"oidc", "provider", status.Error(err)})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -44,20 +45,20 @@ func NewClient(issuerUri *url.URL, oidcClient, oidcSecret, oidcScopes, siteUrl s
|
||||
redirectUrl, urlErr := RedirectURL(siteUrl)
|
||||
|
||||
if urlErr != nil {
|
||||
event.AuditErr([]string{"oidc", "redirect url", "%s"}, err)
|
||||
return nil, err
|
||||
event.AuditErr([]string{"oidc", "redirect url", status.Error(urlErr)})
|
||||
return nil, urlErr
|
||||
}
|
||||
|
||||
// Generate cryptographic keys.
|
||||
var hashKey, encryptKey []byte
|
||||
|
||||
if hashKey, err = rnd.RandomBytes(16); err != nil {
|
||||
event.AuditErr([]string{"oidc", "hash key", "%s"}, err)
|
||||
event.AuditErr([]string{"oidc", "hash key", status.Error(err)})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if encryptKey, err = rnd.RandomBytes(16); err != nil {
|
||||
event.AuditErr([]string{"oidc", "encrypt key", "%s"}, err)
|
||||
event.AuditErr([]string{"oidc", "encrypt key", status.Error(err)})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -85,7 +86,7 @@ func NewClient(issuerUri *url.URL, oidcClient, oidcSecret, oidcScopes, siteUrl s
|
||||
discover, err := client.Discover(context.TODO(), issuerUri.String(), httpClient)
|
||||
|
||||
if err != nil {
|
||||
event.AuditErr([]string{"oidc", "provider", "service discovery", "%s"}, err)
|
||||
event.AuditErr([]string{"oidc", "provider", "service discovery", status.Error(err)})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -110,7 +111,7 @@ func NewClient(issuerUri *url.URL, oidcClient, oidcSecret, oidcScopes, siteUrl s
|
||||
provider, err := rp.NewRelyingPartyOIDC(context.TODO(), issuerUri.String(), oidcClient, oidcSecret, redirectUrl, scopes, clientOpt...)
|
||||
|
||||
if err != nil {
|
||||
event.AuditErr([]string{"oidc", "provider", "%s"}, err)
|
||||
event.AuditErr([]string{"oidc", "provider", status.Error(err)})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// HttpClient returns an HTTP client tailored for OIDC requests. When debug is true, it wraps the
|
||||
@@ -32,7 +33,7 @@ func (lrt LoggingRoundTripper) RoundTrip(req *http.Request) (res *http.Response,
|
||||
|
||||
// Log the request method, URL and error, if any.
|
||||
if err != nil {
|
||||
event.AuditErr([]string{"oidc", "provider", "request", "%s %s", "%s"}, req.Method, req.URL.String(), err)
|
||||
event.AuditErr([]string{"oidc", "provider", "request", "%s %s", status.Error(err)}, req.Method, req.URL.String())
|
||||
} else {
|
||||
event.AuditDebug([]string{"oidc", "provider", "request", "%s %s", "%s"}, req.Method, req.URL.String(), res.Status)
|
||||
}
|
||||
|
||||
@@ -88,11 +88,11 @@ func authJWTKeysListAction(ctx *cli.Context) error {
|
||||
fmt.Println()
|
||||
fmt.Println("JWT signing keys:")
|
||||
for _, row := range rows {
|
||||
status := ""
|
||||
stat := ""
|
||||
if row.Active {
|
||||
status = " (active)"
|
||||
stat = " (active)"
|
||||
}
|
||||
parts := []string{fmt.Sprintf("KID: %s%s", row.Kid, status)}
|
||||
parts := []string{fmt.Sprintf("KID: %s%s", row.Kid, stat)}
|
||||
if row.CreatedAt != "" {
|
||||
parts = append(parts, fmt.Sprintf("created %s", row.CreatedAt))
|
||||
}
|
||||
|
||||
@@ -31,32 +31,32 @@ func authJWTStatusAction(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
ttl := time.Duration(conf.JWKSCacheTTL()) * time.Second
|
||||
status := verifier.Status(ttl)
|
||||
status.JWKSURL = strings.TrimSpace(conf.JWKSUrl())
|
||||
s := verifier.Status(ttl)
|
||||
s.JWKSURL = strings.TrimSpace(conf.JWKSUrl())
|
||||
|
||||
if ctx.Bool("json") {
|
||||
return printJSON(status)
|
||||
return printJSON(s)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf("JWKS URL: %s\n", status.JWKSURL)
|
||||
fmt.Printf("Cache Path: %s\n", status.CachePath)
|
||||
fmt.Printf("Cache URL: %s\n", status.CacheURL)
|
||||
fmt.Printf("Cache ETag: %s\n", status.CacheETag)
|
||||
fmt.Printf("Cached Keys: %d\n", status.KeyCount)
|
||||
if len(status.KeyIDs) > 0 {
|
||||
fmt.Printf("Key IDs: %s\n", strings.Join(status.KeyIDs, ", "))
|
||||
fmt.Printf("JWKS URL: %s\n", s.JWKSURL)
|
||||
fmt.Printf("Cache Path: %s\n", s.CachePath)
|
||||
fmt.Printf("Cache URL: %s\n", s.CacheURL)
|
||||
fmt.Printf("Cache ETag: %s\n", s.CacheETag)
|
||||
fmt.Printf("Cached Keys: %d\n", s.KeyCount)
|
||||
if len(s.KeyIDs) > 0 {
|
||||
fmt.Printf("Key IDs: %s\n", strings.Join(s.KeyIDs, ", "))
|
||||
}
|
||||
if !status.CacheFetchedAt.IsZero() {
|
||||
fmt.Printf("Last Fetch: %s\n", status.CacheFetchedAt.Format(time.RFC3339))
|
||||
if !s.CacheFetchedAt.IsZero() {
|
||||
fmt.Printf("Last Fetch: %s\n", s.CacheFetchedAt.Format(time.RFC3339))
|
||||
} else {
|
||||
fmt.Println("Last Fetch: never")
|
||||
}
|
||||
fmt.Printf("Cache Age: %ds\n", status.CacheAgeSeconds)
|
||||
if status.CacheTTLSeconds > 0 {
|
||||
fmt.Printf("Cache TTL: %ds\n", status.CacheTTLSeconds)
|
||||
fmt.Printf("Cache Age: %ds\n", s.CacheAgeSeconds)
|
||||
if s.CacheTTLSeconds > 0 {
|
||||
fmt.Printf("Cache TTL: %ds\n", s.CacheTTLSeconds)
|
||||
}
|
||||
if status.CacheStale {
|
||||
if s.CacheStale {
|
||||
fmt.Println("Cache Status: STALE")
|
||||
} else {
|
||||
fmt.Println("Cache Status: fresh")
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"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/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/txt/report"
|
||||
)
|
||||
|
||||
@@ -80,7 +81,7 @@ func clusterNodesListAction(ctx *cli.Context) error {
|
||||
event.AuditInfo(append(who,
|
||||
string(acl.ResourceCluster),
|
||||
"list nodes count %d",
|
||||
event.Succeeded,
|
||||
status.Succeeded,
|
||||
), len(out))
|
||||
|
||||
if ctx.Bool("json") {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"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/log/status"
|
||||
)
|
||||
|
||||
// flags for nodes mod
|
||||
@@ -133,7 +134,7 @@ func clusterNodesModAction(ctx *cli.Context) error {
|
||||
args = append(args, clean.Log(changeSummary))
|
||||
}
|
||||
|
||||
segments = append(segments, event.Updated)
|
||||
segments = append(segments, status.Updated)
|
||||
|
||||
event.AuditInfo(append(who, segments...), args...)
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/service/cluster/provisioner"
|
||||
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// ClusterNodesRemoveCommand deletes a node from the registry.
|
||||
@@ -115,7 +116,7 @@ func clusterNodesRemoveAction(ctx *cli.Context) error {
|
||||
event.AuditInfo(append(who,
|
||||
string(acl.ResourceCluster),
|
||||
"node %s",
|
||||
event.Deleted,
|
||||
status.Deleted,
|
||||
), clean.Log(uuid))
|
||||
|
||||
loggedDeletion := false
|
||||
@@ -134,7 +135,7 @@ func clusterNodesRemoveAction(ctx *cli.Context) error {
|
||||
event.AuditInfo(append(who,
|
||||
string(acl.ResourceCluster),
|
||||
"drop database %s user %s",
|
||||
event.Succeeded,
|
||||
status.Succeeded,
|
||||
), clean.Log(dbName), clean.Log(dbUser))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
reg "github.com/photoprism/photoprism/internal/service/cluster/registry"
|
||||
"github.com/photoprism/photoprism/internal/service/cluster/theme"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
"github.com/photoprism/photoprism/pkg/txt/report"
|
||||
)
|
||||
@@ -198,7 +199,7 @@ func clusterNodesRotateAction(ctx *cli.Context) error {
|
||||
segments = append(segments, "%s")
|
||||
args = append(args, clean.Log(detail))
|
||||
}
|
||||
segments = append(segments, event.Succeeded)
|
||||
segments = append(segments, status.Succeeded)
|
||||
|
||||
event.AuditInfo(append(who, segments...), args...)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"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/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/txt/report"
|
||||
)
|
||||
|
||||
@@ -63,7 +64,7 @@ func clusterNodesShowAction(ctx *cli.Context) error {
|
||||
event.AuditInfo(append(who,
|
||||
string(acl.ResourceCluster),
|
||||
"show node %s",
|
||||
event.Succeeded,
|
||||
status.Succeeded,
|
||||
), clean.Log(dto.UUID))
|
||||
|
||||
if ctx.Bool("json") {
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt/report"
|
||||
)
|
||||
@@ -304,7 +305,7 @@ func clusterRegisterAction(ctx *cli.Context) error {
|
||||
event.AuditInfo(append(who,
|
||||
string(acl.ResourceCluster),
|
||||
"register node %s",
|
||||
event.Succeeded,
|
||||
status.Succeeded,
|
||||
), clean.Log(nodeID))
|
||||
|
||||
// Optional persistence
|
||||
|
||||
@@ -52,7 +52,7 @@ func statusAction(ctx *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var status string
|
||||
var response string
|
||||
|
||||
if resp, reqErr := client.Do(req); reqErr != nil {
|
||||
return fmt.Errorf("cannot connect to %s:%d", conf.HttpHost(), conf.HttpPort())
|
||||
@@ -61,10 +61,10 @@ func statusAction(ctx *cli.Context) error {
|
||||
} else if body, readErr := io.ReadAll(resp.Body); readErr != nil {
|
||||
return readErr
|
||||
} else {
|
||||
status = string(body)
|
||||
response = string(body)
|
||||
}
|
||||
|
||||
message := gjson.Get(status, "status").String()
|
||||
message := gjson.Get(response, "status").String()
|
||||
|
||||
if message != "" {
|
||||
fmt.Println(message)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package entity
|
||||
|
||||
import "github.com/photoprism/photoprism/internal/event"
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// AlbumUser maps an album to a user or team and stores the associated permissions.
|
||||
type AlbumUser struct {
|
||||
@@ -44,7 +47,7 @@ func FirstOrCreateAlbumUser(m *AlbumUser) *AlbumUser {
|
||||
if err := Db().Where("uid = ?", m.UID).First(&found).Error; err == nil {
|
||||
return &found
|
||||
} else if err = m.Create(); err != nil {
|
||||
event.AuditErr([]string{"album %s", "failed to set owner and permissions", "%s"}, m.UID, err)
|
||||
event.AuditErr([]string{"album %s", "failed to set owner and permissions", status.Error(err)}, m.UID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/time/unix"
|
||||
"github.com/photoprism/photoprism/pkg/txt/report"
|
||||
@@ -479,7 +480,7 @@ func (m *Client) UpdateLastActive(save bool) *Client {
|
||||
if !save {
|
||||
return m
|
||||
} else if err := Db().Model(m).UpdateColumn("last_active", m.LastActive).Error; err != nil {
|
||||
event.AuditWarn([]string{"client %s", "failed to update activity timestamp", "%s"}, m.ClientUID, err)
|
||||
event.AuditWarn([]string{"client %s", "failed to update activity timestamp", status.Error(err)}, m.ClientUID)
|
||||
}
|
||||
|
||||
return m
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/time/unix"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
@@ -144,7 +145,7 @@ func (m *Session) Regenerate() *Session {
|
||||
// Skip deleting existing session if session ID is not set (or invalid).
|
||||
} else if err := m.Delete(); err != nil {
|
||||
// Failed to delete existing session.
|
||||
event.AuditErr([]string{m.IP(), "session %s", "failed to delete", "%s"}, m.RefID, err)
|
||||
event.AuditErr([]string{m.IP(), "session %s", "failed to delete", status.Error(err)}, m.RefID)
|
||||
} else {
|
||||
// Successfully deleted existing session.
|
||||
event.AuditErr([]string{m.IP(), "session %s", "deleted"}, m.RefID)
|
||||
@@ -856,14 +857,14 @@ func (m *Session) UpdateLastActive(save bool) *Session {
|
||||
if !save {
|
||||
return m
|
||||
} else if err := Db().Model(m).UpdateColumn("last_active", m.LastActive).Error; err != nil {
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "failed to update activity timestamp", "%s"}, m.RefID, err)
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "failed to update activity timestamp", status.Error(err)}, m.RefID)
|
||||
}
|
||||
|
||||
// Update the activity timestamp of the parent session, if any.
|
||||
if m.GetMethod().IsNot(authn.MethodSession) || m.AuthID == "" || m.AuthID == m.ID {
|
||||
return m
|
||||
} else if err := Db().Table(Session{}.TableName()).Where("id = ?", m.AuthID).UpdateColumn("last_active", m.LastActive).Error; err != nil {
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "failed to update activity timestamp of parent session", "%s"}, m.RefID, err)
|
||||
event.AuditWarn([]string{m.IP(), "session %s", "failed to update activity timestamp of parent session", status.Error(err)}, m.RefID)
|
||||
}
|
||||
|
||||
return m
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
@@ -35,7 +36,7 @@ func FindSession(id string) (*Session, error) {
|
||||
cached.UpdateLastActive(cached.LastActive <= 0)
|
||||
return cached, nil
|
||||
} else if err := cached.Delete(); err != nil {
|
||||
event.AuditErr([]string{cached.IP(), "session %s", "failed to delete after expiration", "%s"}, cached.RefID, err)
|
||||
event.AuditErr([]string{cached.IP(), "session %s", "failed to delete after expiration", status.Error(err)}, cached.RefID)
|
||||
}
|
||||
} else if res := Db().First(&found, "id = ?", id); res.RecordNotFound() {
|
||||
return found, fmt.Errorf("invalid session")
|
||||
@@ -49,7 +50,7 @@ func FindSession(id string) (*Session, error) {
|
||||
CacheSession(found, SessionCacheDuration)
|
||||
return found, nil
|
||||
} else if err := found.Delete(); err != nil {
|
||||
event.AuditErr([]string{found.IP(), "session %s", "failed to delete after expiration", "%s"}, found.RefID, err)
|
||||
event.AuditErr([]string{found.IP(), "session %s", "failed to delete after expiration", status.Error(err)}, found.RefID)
|
||||
}
|
||||
|
||||
return found, fmt.Errorf("session expired")
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/time/unix"
|
||||
)
|
||||
@@ -49,13 +50,13 @@ func DeleteChildSessions(s *Session) (deleted int) {
|
||||
found := Sessions{}
|
||||
|
||||
if err := Db().Where("auth_id = ? AND auth_method = ?", s.ID, authn.MethodSession.String()).Find(&found).Error; err != nil {
|
||||
event.AuditErr([]string{"failed to find child sessions", "%s"}, err)
|
||||
event.AuditErr([]string{"failed to find child sessions", status.Error(err)})
|
||||
return deleted
|
||||
}
|
||||
|
||||
for _, sess := range found {
|
||||
if err := sess.Delete(); err != nil {
|
||||
event.AuditErr([]string{sess.IP(), "session %s", "failed to delete child session %s", "%s"}, s.RefID, sess.RefID, err)
|
||||
event.AuditErr([]string{sess.IP(), "session %s", "failed to delete child session %s", status.Error(err)}, s.RefID, sess.RefID)
|
||||
} else {
|
||||
deleted++
|
||||
}
|
||||
@@ -95,13 +96,13 @@ func DeleteClientSessions(client *Client, authMethod authn.MethodType, limit int
|
||||
found := Sessions{}
|
||||
|
||||
if err := q.Find(&found).Error; err != nil {
|
||||
event.AuditErr([]string{"failed to fetch client sessions", "%s"}, err)
|
||||
event.AuditErr([]string{"failed to fetch client sessions", status.Error(err)})
|
||||
return deleted
|
||||
}
|
||||
|
||||
for _, sess := range found {
|
||||
if err := sess.Delete(); err != nil {
|
||||
event.AuditErr([]string{sess.IP(), "session %s", "failed to delete", "%s"}, sess.RefID, err)
|
||||
event.AuditErr([]string{sess.IP(), "session %s", "failed to delete", status.Error(err)}, sess.RefID)
|
||||
} else {
|
||||
deleted++
|
||||
}
|
||||
@@ -115,13 +116,13 @@ func DeleteExpiredSessions() (deleted int) {
|
||||
found := Sessions{}
|
||||
|
||||
if err := Db().Where("sess_expires > 0 AND sess_expires < ?", unix.Now()).Find(&found).Error; err != nil {
|
||||
event.AuditErr([]string{"failed to fetch expired sessions", "%s"}, err)
|
||||
event.AuditErr([]string{"failed to fetch expired sessions", status.Error(err)})
|
||||
return deleted
|
||||
}
|
||||
|
||||
for _, sess := range found {
|
||||
if err := sess.Delete(); err != nil {
|
||||
event.AuditErr([]string{sess.IP(), "session %s", "failed to delete", "%s"}, sess.RefID, err)
|
||||
event.AuditErr([]string{sess.IP(), "session %s", "failed to delete", status.Error(err)}, sess.RefID)
|
||||
} else {
|
||||
deleted++
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
@@ -155,7 +156,7 @@ func AuthLocal(user *User, frm form.Login, s *Session, c *gin.Context) (provider
|
||||
s.SessExpires = authSess.SessExpires
|
||||
}
|
||||
|
||||
event.AuditInfo([]string{clientIp, "session %s", "login as %s", "app password", authn.Succeeded}, s.RefID, clean.LogQuote(username))
|
||||
event.AuditInfo([]string{clientIp, "session %s", "login as %s", "app password", status.Succeeded}, s.RefID, clean.LogQuote(username))
|
||||
event.LoginInfo(clientIp, "api", username, s.UserAgent)
|
||||
}
|
||||
|
||||
@@ -226,7 +227,7 @@ func AuthLocal(user *User, frm form.Login, s *Session, c *gin.Context) (provider
|
||||
}
|
||||
|
||||
if s != nil {
|
||||
event.AuditInfo([]string{clientIp, "session %s", "login as %s", authn.Succeeded}, s.RefID, clean.LogQuote(username))
|
||||
event.AuditInfo([]string{clientIp, "session %s", "login as %s", status.Succeeded}, s.RefID, clean.LogQuote(username))
|
||||
event.LoginInfo(clientIp, "api", username, s.UserAgent)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/list"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
@@ -180,7 +181,7 @@ func FirstOrCreateUser(m *User) *User {
|
||||
if found := FindUser(*m); found != nil {
|
||||
return found
|
||||
} else if err := m.Create(); err != nil {
|
||||
event.AuditErr([]string{"user", "failed to create", "%s"}, err)
|
||||
event.AuditErr([]string{"user", "failed to create", status.Error(err)})
|
||||
return nil
|
||||
} else {
|
||||
return m
|
||||
@@ -281,14 +282,14 @@ func (m *User) InitAccount(initName, initPasswd string) (updated bool) {
|
||||
|
||||
// Save password.
|
||||
if err := initialPasswd.Save(); err != nil {
|
||||
event.AuditErr([]string{"user %s", "failed to change password", "%s"}, m.RefID, err)
|
||||
event.AuditErr([]string{"user %s", "failed to change password", status.Error(err)}, m.RefID)
|
||||
return false
|
||||
}
|
||||
|
||||
// Change username if needed.
|
||||
if initName != "" && initName != m.UserName {
|
||||
if err := m.UpdateUsername(initName); err != nil {
|
||||
event.AuditErr([]string{"user %s", "failed to change username to %s", "%s"}, m.RefID, clean.Log(initName), err)
|
||||
event.AuditErr([]string{"user %s", "failed to change username to %s", status.Error(err)}, m.RefID, clean.Log(initName))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +329,7 @@ func (m *User) Delete() (err error) {
|
||||
}
|
||||
|
||||
if err = UnscopedDb().Delete(Session{}, "user_uid = ?", m.UserUID).Error; err != nil {
|
||||
event.AuditErr([]string{"user %s", "delete", "failed to remove sessions", "%s"}, m.RefID, err)
|
||||
event.AuditErr([]string{"user %s", "delete", "failed to remove sessions", status.Error(err)}, m.RefID)
|
||||
}
|
||||
|
||||
err = Db().Delete(m).Error
|
||||
@@ -359,10 +360,10 @@ func (m *User) LoadRelated() *User {
|
||||
// SaveRelated saves related settings and details.
|
||||
func (m *User) SaveRelated() *User {
|
||||
if err := m.Settings().Save(); err != nil {
|
||||
event.AuditErr([]string{"user %s", "failed to save settings", "%s"}, m.RefID, err)
|
||||
event.AuditErr([]string{"user %s", "failed to save settings", status.Error(err)}, m.RefID)
|
||||
}
|
||||
if err := m.Details().Save(); err != nil {
|
||||
event.AuditErr([]string{"user %s", "failed to save details", "%s"}, m.RefID, err)
|
||||
event.AuditErr([]string{"user %s", "failed to save details", status.Error(err)}, m.RefID)
|
||||
}
|
||||
|
||||
return m
|
||||
@@ -630,7 +631,7 @@ func (m *User) SetAuthID(id, issuer string) *User {
|
||||
if err := UnscopedDb().Model(&User{}).
|
||||
Where("user_uid <> ? AND auth_provider = ? AND auth_id = ? AND super_admin = 0", m.UserUID, m.AuthProvider, m.AuthID).
|
||||
Updates(Values{"auth_id": "", "auth_provider": authn.ProviderNone}).Error; err != nil {
|
||||
event.AuditErr([]string{"user %s", "failed to resolve auth id conflicts", "%s"}, m.RefID, err)
|
||||
event.AuditErr([]string{"user %s", "failed to resolve auth id conflicts", status.Error(err)}, m.RefID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -902,14 +903,14 @@ func (m *User) DeleteSessions(omit []string) (deleted int) {
|
||||
sess := Sessions{}
|
||||
|
||||
if err := stmt.Find(&sess).Error; err != nil {
|
||||
event.AuditErr([]string{"user %s", "failed to invalidate sessions", "%s"}, m.RefID, err)
|
||||
event.AuditErr([]string{"user %s", "failed to invalidate sessions", status.Error(err)}, m.RefID)
|
||||
return 0
|
||||
}
|
||||
|
||||
// Delete sessions from cache and database.
|
||||
for _, s := range sess {
|
||||
if err := s.Delete(); err != nil {
|
||||
event.AuditWarn([]string{"user %s", "failed to invalidate session %s", "%s"}, m.RefID, clean.Log(s.RefID), err)
|
||||
event.AuditWarn([]string{"user %s", "failed to invalidate session %s", status.Error(err)}, m.RefID, clean.Log(s.RefID))
|
||||
} else {
|
||||
deleted++
|
||||
}
|
||||
@@ -957,7 +958,7 @@ func (m *User) DeletePassword() (err error) {
|
||||
|
||||
// Remove local account password.
|
||||
if err = pw.Delete(); err != nil {
|
||||
event.AuditErr([]string{"user %s", "failed to remove password", "%s"}, m.RefID, err)
|
||||
event.AuditErr([]string{"user %s", "failed to remove password", status.Error(err)}, m.RefID)
|
||||
} else {
|
||||
event.AuditWarn([]string{"user %s", "password has been removed"}, m.RefID)
|
||||
}
|
||||
@@ -1239,12 +1240,12 @@ func (m *User) RedeemToken(token string) (n int) {
|
||||
share.Comment = link.Comment
|
||||
|
||||
if err := share.Save(); err != nil {
|
||||
event.AuditErr([]string{"user %s", "token %s", "failed to redeem shares", "%s"}, m.RefID, clean.Log(token), err)
|
||||
event.AuditErr([]string{"user %s", "token %s", "failed to redeem shares", status.Error(err)}, m.RefID, clean.Log(token))
|
||||
} else {
|
||||
link.Redeem()
|
||||
}
|
||||
} else if err := found.UpdateLink(link); err != nil {
|
||||
event.AuditErr([]string{"user %s", "token %s", "failed to update shares", "%s"}, m.RefID, clean.Log(token), err)
|
||||
event.AuditErr([]string{"user %s", "token %s", "failed to update shares", status.Error(err)}, m.RefID, clean.Log(token))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/auth/acl"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// Role defaults referenced when creating built-in users.
|
||||
@@ -97,7 +98,7 @@ func CreateDefaultUsers() {
|
||||
|
||||
// Add initial admin account.
|
||||
if err := Admin.Create(); err != nil {
|
||||
event.AuditErr([]string{"user", "failed to create", "%s"}, err)
|
||||
event.AuditErr([]string{"user", "failed to create", status.Error(err)})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
@@ -120,7 +121,7 @@ func FindUserShares(userUid string) UserShares {
|
||||
|
||||
// Find matching record.
|
||||
if err := UnscopedDb().Find(&found, "user_uid = ? AND (expires_at IS NULL OR expires_at > ?)", userUid, Now()).Error; err != nil {
|
||||
event.AuditWarn([]string{"user %s", "find shares", "%s"}, clean.Log(userUid), err)
|
||||
event.AuditWarn([]string{"user %s", "find shares", status.Error(err)}, clean.Log(userUid))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
@@ -83,7 +84,7 @@ func (m *Link) Redeem() *Link {
|
||||
m.LinkViews += 1
|
||||
|
||||
if err := Db().Model(m).UpdateColumn("link_views", gorm.Expr("link_views + 1")).Error; err != nil {
|
||||
event.AuditWarn([]string{"link %s", "update views", "%s"}, clean.Log(m.RefID), clean.Error(err))
|
||||
event.AuditWarn([]string{"link %s", "update views", status.Error(err)}, clean.Log(m.RefID))
|
||||
}
|
||||
|
||||
return m
|
||||
@@ -172,7 +173,7 @@ func (m *Link) Delete() error {
|
||||
|
||||
// Remove related user shares.
|
||||
if err := UnscopedDb().Delete(UserShare{}, "link_uid = ?", m.LinkUID).Error; err != nil {
|
||||
event.AuditErr([]string{"link %s", "failed to remove related user shares", "%s"}, clean.Log(m.RefID), err)
|
||||
event.AuditErr([]string{"link %s", "failed to remove related user shares", status.Error(err)}, clean.Log(m.RefID))
|
||||
}
|
||||
|
||||
return Db().Delete(m).Error
|
||||
@@ -186,7 +187,7 @@ func DeleteShareLinks(shareUid string) error {
|
||||
|
||||
// Remove related user shares.
|
||||
if err := UnscopedDb().Delete(UserShare{}, "share_uid = ?", shareUid).Error; err != nil {
|
||||
event.AuditErr([]string{"share %s", "failed to remove related user shares", "%s"}, clean.Log(shareUid), err)
|
||||
event.AuditErr([]string{"share %s", "failed to remove related user shares", status.Error(err)}, clean.Log(shareUid))
|
||||
}
|
||||
|
||||
return Db().Delete(&Link{}, "share_uid = ?", shareUid).Error
|
||||
@@ -232,7 +233,7 @@ func FindLinks(token, shared string) (found Links) {
|
||||
}
|
||||
|
||||
if err := q.Order("modified_at DESC").Find(&found).Error; err != nil {
|
||||
event.AuditErr([]string{"token %s", "%s"}, clean.Log(token), err)
|
||||
event.AuditErr([]string{"token %s", status.Error(err)}, clean.Log(token))
|
||||
}
|
||||
|
||||
return found
|
||||
|
||||
@@ -10,18 +10,18 @@ import (
|
||||
)
|
||||
|
||||
// Status returns the current status of schema migrations.
|
||||
func Status(db *gorm.DB, ids []string) (status Migrations, err error) {
|
||||
status = Migrations{}
|
||||
func Status(db *gorm.DB, ids []string) (result Migrations, err error) {
|
||||
result = Migrations{}
|
||||
|
||||
if db == nil {
|
||||
return status, fmt.Errorf("migrate: no database connection")
|
||||
return result, fmt.Errorf("migrate: no database connection")
|
||||
}
|
||||
|
||||
// Get SQL dialect name.
|
||||
name := db.Dialect().GetName()
|
||||
|
||||
if name == "" {
|
||||
return status, fmt.Errorf("migrate: failed to determine sql dialect")
|
||||
return result, fmt.Errorf("migrate: failed to determine sql dialect")
|
||||
}
|
||||
|
||||
// Make sure a "migrations" table exists.
|
||||
@@ -30,13 +30,13 @@ func Status(db *gorm.DB, ids []string) (status Migrations, err error) {
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return status, fmt.Errorf("migrate: %s (create migrations table)", err)
|
||||
return result, fmt.Errorf("migrate: %s (create migrations table)", err)
|
||||
}
|
||||
|
||||
migrations, ok := Dialects[name]
|
||||
|
||||
if !ok && len(migrations) == 0 {
|
||||
return status, fmt.Errorf("migrate: no migrations found for %s", name)
|
||||
return result, fmt.Errorf("migrate: no migrations found for %s", name)
|
||||
}
|
||||
|
||||
// Find previously executed migrations.
|
||||
@@ -62,12 +62,12 @@ func Status(db *gorm.DB, ids []string) (status Migrations, err error) {
|
||||
migration.Source = done.Source
|
||||
migration.StartedAt = done.StartedAt
|
||||
migration.FinishedAt = done.FinishedAt
|
||||
status = append(status, migration)
|
||||
result = append(result, migration)
|
||||
} else {
|
||||
// Should not happen.
|
||||
status = append(status, migration)
|
||||
result = append(result, migration)
|
||||
}
|
||||
}
|
||||
|
||||
return status, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package entity
|
||||
|
||||
import "github.com/photoprism/photoprism/internal/event"
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
)
|
||||
|
||||
// PhotoUser represents the user and group ownership of a Photo and the corresponding permissions.
|
||||
type PhotoUser struct {
|
||||
@@ -44,7 +47,7 @@ func FirstOrCreatePhotoUser(m *PhotoUser) *PhotoUser {
|
||||
if err := Db().Where("uid = ?", m.UID).First(&found).Error; err == nil {
|
||||
return &found
|
||||
} else if err = m.Create(); err != nil {
|
||||
event.AuditErr([]string{"photo %s", "failed to set owner and permissions", "%s"}, m.UID, err)
|
||||
event.AuditErr([]string{"photo %s", "failed to set owner and permissions", status.Error(err)}, m.UID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity/sortby"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/enum"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/geo"
|
||||
"github.com/photoprism/photoprism/pkg/geo/pluscode"
|
||||
"github.com/photoprism/photoprism/pkg/geo/s2"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
@@ -159,7 +159,7 @@ func searchPhotos(frm form.SearchPhotos, sess *entity.Session, resultCols string
|
||||
// Visitors and other restricted users can only access shared content.
|
||||
if frm.Scope != "" && album.CreatedBy != user.UserUID && !sess.HasShare(frm.Scope) && (sess.GetUser().HasSharedAccessOnly(acl.ResourcePhotos) || sess.NotRegistered()) ||
|
||||
frm.Scope == "" && acl.Rules.Deny(acl.ResourcePhotos, aclRole, acl.ActionSearch) {
|
||||
event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", authn.Denied}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePhotos), aclRole)
|
||||
event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", status.Denied}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePhotos), aclRole)
|
||||
return PhotoResults{}, 0, ErrForbidden
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/enum"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/geo"
|
||||
"github.com/photoprism/photoprism/pkg/geo/pluscode"
|
||||
"github.com/photoprism/photoprism/pkg/geo/s2"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
@@ -137,7 +137,7 @@ func UserPhotosGeo(frm form.SearchPhotosGeo, sess *entity.Session) (results GeoR
|
||||
// Visitors and other restricted users can only access shared content.
|
||||
if frm.Scope != "" && album.CreatedBy != user.UserUID && !sess.HasShare(frm.Scope) && (sess.GetUser().HasSharedAccessOnly(acl.ResourcePlaces) || sess.NotRegistered()) ||
|
||||
frm.Scope == "" && acl.Rules.Deny(acl.ResourcePlaces, aclRole, acl.ActionSearch) {
|
||||
event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", authn.Denied}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePlaces), aclRole)
|
||||
event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", status.Denied}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePlaces), aclRole)
|
||||
return GeoResults{}, ErrForbidden
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
@@ -103,14 +104,14 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
limiter.Auth.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "webdav", "access as %s with authorization granted to %s", authn.Denied}, clean.Log(username), clean.Log(user.Username()))
|
||||
event.AuditErr([]string{clientIp, "webdav", "access as %s with authorization granted to %s", status.Denied}, clean.Log(username), clean.Log(user.Username()))
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
} else if sess == nil {
|
||||
// Ignore and try basic auth next.
|
||||
} else if !sess.HasUser() || user == nil {
|
||||
// Log error if session does not belong to an authorized user account.
|
||||
event.AuditErr([]string{clientIp, "webdav", "client %s", "session %s", "access without user account", authn.Denied}, clean.Log(sess.GetClientInfo()), sess.RefID)
|
||||
event.AuditErr([]string{clientIp, "webdav", "client %s", "session %s", "access without user account", status.Denied}, clean.Log(sess.GetClientInfo()), sess.RefID)
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
} else if sess.IsClient() && sess.InsufficientScope(acl.ResourceWebDAV, nil) {
|
||||
@@ -143,7 +144,7 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||
sess.UpdateLastActive(true)
|
||||
|
||||
// Log successful authentication.
|
||||
event.AuditInfo([]string{clientIp, "webdav", "client %s", "session %s", "access as %s", authn.Succeeded}, clean.Log(sess.GetClientInfo()), sess.RefID, clean.LogQuote(user.Username()))
|
||||
event.AuditInfo([]string{clientIp, "webdav", "client %s", "session %s", "access as %s", status.Succeeded}, clean.Log(sess.GetClientInfo()), sess.RefID, clean.LogQuote(user.Username()))
|
||||
event.LoginInfo(clientIp, "webdav", user.Username(), api.UserAgent(c))
|
||||
|
||||
// Cache authentication to improve performance.
|
||||
@@ -213,7 +214,7 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||
r.Success()
|
||||
|
||||
// Log successful authentication.
|
||||
event.AuditInfo([]string{clientIp, "webdav", "login as %s", authn.Succeeded}, clean.LogQuote(username))
|
||||
event.AuditInfo([]string{clientIp, "webdav", "login as %s", status.Succeeded}, clean.LogQuote(username))
|
||||
event.LoginInfo(clientIp, "webdav", username, api.UserAgent(c))
|
||||
|
||||
// Cache authentication to improve performance.
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/server/limiter"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/http/header"
|
||||
"github.com/photoprism/photoprism/pkg/log/status"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
@@ -48,7 +48,7 @@ func WebDAVAuthSession(c *gin.Context, authToken string) (sess *entity.Session,
|
||||
// Count error towards failure rate limit, emits audit event, and returns nil?
|
||||
if sess == nil || err != nil {
|
||||
limiter.Auth.Reserve(clientIp)
|
||||
event.AuditErr([]string{header.ClientIP(c), "webdav", "access with invalid auth token", authn.Denied})
|
||||
event.AuditErr([]string{header.ClientIP(c), "webdav", "access with invalid auth token", status.Denied})
|
||||
return nil, nil, sid, false
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,6 @@ package authn
|
||||
|
||||
// Generic status messages for authentication and authorization:
|
||||
const (
|
||||
Failed = "failed"
|
||||
Denied = "denied"
|
||||
Granted = "granted"
|
||||
Created = "created"
|
||||
Succeeded = "succeeded"
|
||||
Verified = "verified"
|
||||
Activated = "activated"
|
||||
Deactivated = "deactivated"
|
||||
Passcode = "passcode"
|
||||
Session = "session"
|
||||
Sessions = "sessions"
|
||||
|
||||
@@ -23,7 +23,7 @@ func Error(err error) string {
|
||||
switch r {
|
||||
case '`', '"':
|
||||
return '\''
|
||||
case '\\', '$', '<', '>', '{', '}':
|
||||
case '%', '\\', '$', '<', '>', '{', '}':
|
||||
return '?'
|
||||
default:
|
||||
return r
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package event
|
||||
package status
|
||||
|
||||
// Generic outcomes for use in system and audit logs.
|
||||
const (
|
||||
Succeeded = "succeeded"
|
||||
Failed = "failed"
|
||||
Denied = "denied"
|
||||
Granted = "granted"
|
||||
Created = "created"
|
||||
Added = "added"
|
||||
Updated = "updated"
|
||||
Created = "created"
|
||||
Deleted = "deleted"
|
||||
Removed = "removed"
|
||||
Succeeded = "succeeded"
|
||||
NotFound = "not found"
|
||||
Verified = "verified"
|
||||
Activated = "activated"
|
||||
Deactivated = "deactivated"
|
||||
Joined = "joined"
|
||||
Confirmed = "confirmed"
|
||||
Synced = "synced"
|
||||
)
|
||||
12
pkg/log/status/error.go
Normal file
12
pkg/log/status/error.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
// Error returns a sanitized string representation of err for use in audit and
|
||||
// system logs, for example when an error message should be the final outcome
|
||||
// token in an `event.Audit*` slice.
|
||||
func Error(err error) string {
|
||||
return clean.Error(err)
|
||||
}
|
||||
42
pkg/log/status/error_test.go
Normal file
42
pkg/log/status/error_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package status
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
)
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
name: "Nil",
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
name: "SanitizeSpecialCharacters",
|
||||
err: errors.New("permission denied { DROP TABLE users; }\n"),
|
||||
},
|
||||
{
|
||||
name: "WhitespaceOnly",
|
||||
err: errors.New(" "),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if got, want := Error(tt.err), clean.Error(tt.err); got != want {
|
||||
t.Fatalf("Error(%v) = %q, want %q", tt.err, got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
26
pkg/log/status/status.go
Normal file
26
pkg/log/status/status.go
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
Package status defines canonical outcome tokens shared by audit and system
|
||||
logging helpers so events stay consistent across packages.
|
||||
|
||||
Copyright (c) 2018 - 2025 PhotoPrism UG. All rights reserved.
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under Version 3 of the GNU Affero General Public License (the "AGPL"):
|
||||
<https://docs.photoprism.app/license/agpl>
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
The AGPL is supplemented by our Trademark and Brand Guidelines,
|
||||
which describe how our Brand Assets may be used:
|
||||
<https://www.photoprism.app/trademark>
|
||||
|
||||
Feel free to send an email to hello@photoprism.app if you have questions,
|
||||
want to support our work, or just want to say hello.
|
||||
|
||||
Additional information can be found in our Developer Guide:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package status
|
||||
Reference in New Issue
Block a user