Share: Improve query validation in the search and albums API

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-10-07 17:33:04 +02:00
parent 0f321b10bc
commit 80dd926f2d
5 changed files with 111 additions and 16 deletions

View File

@ -49,8 +49,17 @@ func GetAlbum(router *gin.RouterGroup) {
return
}
id := clean.UID(c.Param("uid"))
a, err := query.AlbumByUID(id)
// Get sanitized album UID from request path.
uid := clean.UID(c.Param("uid"))
// Visitors and other restricted users can only access shared content.
if (s.User().HasSharedAccessOnly(acl.ResourceAlbums) || s.NotRegistered()) && !s.HasShare(uid) {
AbortForbidden(c)
return
}
// Find album by UID.
a, err := query.AlbumByUID(uid)
if err != nil {
AbortAlbumNotFound(c)
@ -129,7 +138,16 @@ func UpdateAlbum(router *gin.RouterGroup) {
return
}
// Get sanitized album UID from request path.
uid := clean.UID(c.Param("uid"))
// Visitors and other restricted users can only access shared content.
if (s.User().HasSharedAccessOnly(acl.ResourceAlbums) || s.NotRegistered()) && !s.HasShare(uid) {
AbortForbidden(c)
return
}
// Find album by UID.
a, err := query.AlbumByUID(uid)
if err != nil {
@ -184,9 +202,17 @@ func DeleteAlbum(router *gin.RouterGroup) {
return
}
id := clean.UID(c.Param("uid"))
// Get sanitized album UID from request path.
uid := clean.UID(c.Param("uid"))
a, err := query.AlbumByUID(id)
// Visitors and other restricted users can only access shared content.
if (s.User().HasSharedAccessOnly(acl.ResourceAlbums) || s.NotRegistered()) && !s.HasShare(uid) {
AbortForbidden(c)
return
}
// Find album by UID.
a, err := query.AlbumByUID(uid)
if err != nil {
AbortAlbumNotFound(c)
@ -211,7 +237,7 @@ func DeleteAlbum(router *gin.RouterGroup) {
return
}
// PublishAlbumEvent(EntityDeleted, id, c)
// PublishAlbumEvent(EntityDeleted, uid, c)
UpdateClientConfig()
@ -237,8 +263,17 @@ func LikeAlbum(router *gin.RouterGroup) {
return
}
id := clean.UID(c.Param("uid"))
a, err := query.AlbumByUID(id)
// Get sanitized album UID from request path.
uid := clean.UID(c.Param("uid"))
// Visitors and other restricted users can only access shared content.
if (s.User().HasSharedAccessOnly(acl.ResourceAlbums) || s.NotRegistered()) && !s.HasShare(uid) {
AbortForbidden(c)
return
}
// Find album by UID.
a, err := query.AlbumByUID(uid)
if err != nil {
AbortAlbumNotFound(c)
@ -252,7 +287,7 @@ func LikeAlbum(router *gin.RouterGroup) {
UpdateClientConfig()
PublishAlbumEvent(EntityUpdated, id, c)
PublishAlbumEvent(EntityUpdated, uid, c)
// Update album YAML backup.
SaveAlbumAsYaml(a)
@ -276,8 +311,17 @@ func DislikeAlbum(router *gin.RouterGroup) {
return
}
id := clean.UID(c.Param("uid"))
a, err := query.AlbumByUID(id)
// Get sanitized album UID from request path.
uid := clean.UID(c.Param("uid"))
// Visitors and other restricted users can only access shared content.
if (s.User().HasSharedAccessOnly(acl.ResourceAlbums) || s.NotRegistered()) && !s.HasShare(uid) {
AbortForbidden(c)
return
}
// Find album by UID.
a, err := query.AlbumByUID(uid)
if err != nil {
AbortAlbumNotFound(c)
@ -291,7 +335,7 @@ func DislikeAlbum(router *gin.RouterGroup) {
UpdateClientConfig()
PublishAlbumEvent(EntityUpdated, id, c)
PublishAlbumEvent(EntityUpdated, uid, c)
// Update album YAML backup.
SaveAlbumAsYaml(a)
@ -311,7 +355,17 @@ func CloneAlbums(router *gin.RouterGroup) {
return
}
a, err := query.AlbumByUID(clean.UID(c.Param("uid")))
// Get sanitized album UID from request path.
uid := clean.UID(c.Param("uid"))
// Visitors and other restricted users can only access shared content.
if (s.User().HasSharedAccessOnly(acl.ResourceAlbums) || s.NotRegistered()) && !s.HasShare(uid) {
AbortForbidden(c)
return
}
// Find album by UID.
a, err := query.AlbumByUID(uid)
if err != nil {
AbortAlbumNotFound(c)
@ -376,7 +430,16 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
return
}
// Get sanitized album UID from request path.
uid := clean.UID(c.Param("uid"))
// Visitors and other restricted users can only access shared content.
if (s.User().HasSharedAccessOnly(acl.ResourceAlbums) || s.NotRegistered()) && !s.HasShare(uid) {
AbortForbidden(c)
return
}
// Find album by UID.
a, err := query.AlbumByUID(uid)
if err != nil {
@ -443,7 +506,17 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) {
return
}
a, err := query.AlbumByUID(clean.UID(c.Param("uid")))
// Get sanitized album UID from request path.
uid := clean.UID(c.Param("uid"))
// Visitors and other restricted users can only access shared content.
if (s.User().HasSharedAccessOnly(acl.ResourceAlbums) || s.NotRegistered()) && !s.HasShare(uid) {
AbortForbidden(c)
return
}
// Find album by UID.
a, err := query.AlbumByUID(uid)
if err != nil {
AbortAlbumNotFound(c)

View File

@ -731,6 +731,15 @@ func (m *User) IsVisitor() bool {
return m.AclRole() == acl.RoleVisitor || m.ID == Visitor.ID
}
// HasSharedAccessOnly checks if the user as only access to shared resources.
func (m *User) HasSharedAccessOnly(resource acl.Resource) bool {
if acl.Resources.Deny(resource, m.AclRole(), acl.AccessShared) {
return false
}
return acl.Resources.DenyAll(resource, m.AclRole(), acl.Permissions{acl.AccessAll, acl.AccessLibrary})
}
// IsUnknown checks if the user is unknown.
func (m *User) IsUnknown() bool {
return !rnd.IsUID(m.UserUID, UserUID) || m.ID == UnknownUser.ID || m.UserUID == UnknownUser.UserUID
@ -953,10 +962,17 @@ func (m *User) HasShares() bool {
// HasShare if a uid was shared with the user.
func (m *User) HasShare(uid string) bool {
if !m.IsRegistered() || m.NoShares() {
if !m.IsRegistered() {
return false
}
// Check cashed list of user shares.
if m.UserShares.Contains(uid) {
return true
}
// Refresh cache and try again if not found.
m.RefreshShares()
return m.UserShares.Contains(uid)
}

View File

@ -43,6 +43,8 @@ func TestFindLocalUser(t *testing.T) {
assert.Equal(t, acl.RoleAdmin, m.AclRole())
assert.Equal(t, "", m.Attr())
assert.False(t, m.IsVisitor())
assert.False(t, m.HasSharedAccessOnly(acl.ResourcePhotos))
assert.False(t, m.HasSharedAccessOnly(acl.ResourceAlbums))
assert.True(t, m.SuperAdmin)
assert.True(t, m.CanLogin)
assert.True(t, m.CanInvite)
@ -66,6 +68,8 @@ func TestFindLocalUser(t *testing.T) {
assert.Equal(t, acl.RoleAdmin, m.AclRole())
assert.NotEqual(t, acl.RoleVisitor, m.AclRole())
assert.False(t, m.IsVisitor())
assert.False(t, m.HasSharedAccessOnly(acl.ResourcePhotos))
assert.False(t, m.HasSharedAccessOnly(acl.ResourceAlbums))
assert.True(t, m.CanLogin)
assert.NotEmpty(t, m.CreatedAt)
assert.NotEmpty(t, m.UpdatedAt)
@ -85,6 +89,8 @@ func TestFindLocalUser(t *testing.T) {
assert.Equal(t, "bob@example.com", m.UserEmail)
assert.False(t, m.SuperAdmin)
assert.False(t, m.IsVisitor())
assert.False(t, m.HasSharedAccessOnly(acl.ResourcePhotos))
assert.False(t, m.HasSharedAccessOnly(acl.ResourceAlbums))
assert.True(t, m.CanLogin)
assert.NotEmpty(t, m.CreatedAt)
assert.NotEmpty(t, m.UpdatedAt)

View File

@ -146,7 +146,7 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
}
// Visitors and other restricted users can only access shared content.
if f.Scope != "" && !sess.HasShare(f.Scope) && (sess.IsVisitor() || sess.NotRegistered()) ||
if f.Scope != "" && !sess.HasShare(f.Scope) && (sess.User().HasSharedAccessOnly(acl.ResourcePhotos) || sess.NotRegistered()) ||
f.Scope == "" && acl.Resources.Deny(acl.ResourcePhotos, aclRole, acl.ActionSearch) {
event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", "denied"}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePhotos), aclRole)
return PhotoResults{}, 0, ErrForbidden

View File

@ -129,7 +129,7 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
}
// Visitors and other restricted users can only access shared content.
if f.Scope != "" && !sess.HasShare(f.Scope) && (sess.IsVisitor() || sess.NotRegistered()) ||
if f.Scope != "" && !sess.HasShare(f.Scope) && (sess.User().HasSharedAccessOnly(acl.ResourcePlaces) || sess.NotRegistered()) ||
f.Scope == "" && acl.Resources.Deny(acl.ResourcePlaces, aclRole, acl.ActionSearch) {
event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", "denied"}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePlaces), aclRole)
return GeoResults{}, ErrForbidden