mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -27,7 +27,7 @@ package acl
|
||||
// ACL represents an access control list based on Resource, Roles, and Permissions.
|
||||
type ACL map[Resource]Roles
|
||||
|
||||
// Deny checks whether the role must be denied access to the specified resource.
|
||||
// Deny checks whether the Role must be denied access to the specified Resource.
|
||||
func (acl ACL) Deny(resource Resource, role Role, perm Permission) bool {
|
||||
return !acl.Allow(resource, role, perm)
|
||||
}
|
||||
|
||||
@@ -8,130 +8,130 @@ import (
|
||||
|
||||
func TestACL_Allow(t *testing.T) {
|
||||
t.Run("ResourceSessions", func(t *testing.T) {
|
||||
assert.True(t, Resources.Allow(ResourceSessions, RoleAdmin, AccessAll))
|
||||
assert.True(t, Resources.Allow(ResourceSessions, RoleAdmin, AccessOwn))
|
||||
assert.False(t, Resources.Allow(ResourceSessions, RoleVisitor, AccessAll))
|
||||
assert.True(t, Resources.Allow(ResourceSessions, RoleVisitor, AccessOwn))
|
||||
assert.False(t, Resources.Allow(ResourceSessions, RoleClient, AccessAll))
|
||||
assert.True(t, Resources.Allow(ResourceSessions, RoleClient, AccessOwn))
|
||||
assert.True(t, Rules.Allow(ResourceSessions, RoleAdmin, AccessAll))
|
||||
assert.True(t, Rules.Allow(ResourceSessions, RoleAdmin, AccessOwn))
|
||||
assert.False(t, Rules.Allow(ResourceSessions, RoleVisitor, AccessAll))
|
||||
assert.True(t, Rules.Allow(ResourceSessions, RoleVisitor, AccessOwn))
|
||||
assert.False(t, Rules.Allow(ResourceSessions, RoleClient, AccessAll))
|
||||
assert.True(t, Rules.Allow(ResourceSessions, RoleClient, AccessOwn))
|
||||
})
|
||||
t.Run("ResourcePhotosRoleAdminActionModify", func(t *testing.T) {
|
||||
assert.True(t, Resources.Allow(ResourcePhotos, RoleAdmin, ActionUpdate))
|
||||
assert.True(t, Rules.Allow(ResourcePhotos, RoleAdmin, ActionUpdate))
|
||||
})
|
||||
t.Run("ResourceDefaultRoleAdminActionDefault", func(t *testing.T) {
|
||||
assert.True(t, Resources.Allow(ResourceDefault, RoleAdmin, FullAccess))
|
||||
assert.True(t, Rules.Allow(ResourceDefault, RoleAdmin, FullAccess))
|
||||
})
|
||||
t.Run("ResourceDefaultRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.Allow(ResourceDefault, RoleVisitor, FullAccess))
|
||||
assert.False(t, Rules.Allow(ResourceDefault, RoleVisitor, FullAccess))
|
||||
})
|
||||
t.Run("ResourcePhotosRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.Allow(ResourcePhotos, RoleVisitor, FullAccess))
|
||||
assert.False(t, Rules.Allow(ResourcePhotos, RoleVisitor, FullAccess))
|
||||
})
|
||||
t.Run("ResourceAlbumsRoleVisitorAccessShared", func(t *testing.T) {
|
||||
assert.True(t, Resources.Allow(ResourceAlbums, RoleVisitor, AccessShared))
|
||||
assert.True(t, Rules.Allow(ResourceAlbums, RoleVisitor, AccessShared))
|
||||
})
|
||||
t.Run("ResourceAlbumsRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.Allow(ResourceAlbums, RoleVisitor, FullAccess))
|
||||
assert.False(t, Rules.Allow(ResourceAlbums, RoleVisitor, FullAccess))
|
||||
})
|
||||
t.Run("WrongResourceRoleAdminActionDefault", func(t *testing.T) {
|
||||
assert.True(t, Resources.Allow("wrong", RoleAdmin, FullAccess))
|
||||
assert.True(t, Rules.Allow("wrong", RoleAdmin, FullAccess))
|
||||
})
|
||||
t.Run("WrongResourceRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.Allow("wrong", RoleVisitor, FullAccess))
|
||||
assert.False(t, Rules.Allow("wrong", RoleVisitor, FullAccess))
|
||||
})
|
||||
}
|
||||
|
||||
func TestACL_AllowAny(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.False(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{}))
|
||||
assert.False(t, Rules.AllowAny(ResourceAlbums, RoleVisitor, Permissions{}))
|
||||
})
|
||||
t.Run("VisitorAccess", func(t *testing.T) {
|
||||
assert.True(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessAll, AccessShared}))
|
||||
assert.True(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||
assert.False(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessAll}))
|
||||
assert.True(t, Rules.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessAll, AccessShared}))
|
||||
assert.True(t, Rules.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||
assert.False(t, Rules.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessAll}))
|
||||
})
|
||||
t.Run("ResourcePhotosRoleAdminActionModify", func(t *testing.T) {
|
||||
assert.True(t, Resources.AllowAny(ResourcePhotos, RoleAdmin, Permissions{ActionUpdate}))
|
||||
assert.True(t, Rules.AllowAny(ResourcePhotos, RoleAdmin, Permissions{ActionUpdate}))
|
||||
})
|
||||
t.Run("ResourceDefaultRoleAdminActionDefault", func(t *testing.T) {
|
||||
assert.True(t, Resources.AllowAny(ResourceDefault, RoleAdmin, Permissions{FullAccess}))
|
||||
assert.True(t, Rules.AllowAny(ResourceDefault, RoleAdmin, Permissions{FullAccess}))
|
||||
})
|
||||
t.Run("ResourceDefaultRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.AllowAny(ResourceDefault, RoleVisitor, Permissions{FullAccess}))
|
||||
assert.False(t, Rules.AllowAny(ResourceDefault, RoleVisitor, Permissions{FullAccess}))
|
||||
})
|
||||
t.Run("ResourcePhotosRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.AllowAny(ResourcePhotos, RoleVisitor, Permissions{FullAccess}))
|
||||
assert.False(t, Rules.AllowAny(ResourcePhotos, RoleVisitor, Permissions{FullAccess}))
|
||||
})
|
||||
t.Run("ResourceAlbumsRoleVisitorAccessShared", func(t *testing.T) {
|
||||
assert.True(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||
assert.True(t, Rules.AllowAny(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||
})
|
||||
t.Run("ResourceAlbumsRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.AllowAny(ResourceAlbums, RoleVisitor, Permissions{FullAccess}))
|
||||
assert.False(t, Rules.AllowAny(ResourceAlbums, RoleVisitor, Permissions{FullAccess}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestACL_AllowAll(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.False(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{}))
|
||||
assert.False(t, Rules.AllowAll(ResourceAlbums, RoleVisitor, Permissions{}))
|
||||
})
|
||||
t.Run("VisitorAccess", func(t *testing.T) {
|
||||
assert.False(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessAll, AccessShared}))
|
||||
assert.True(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||
assert.False(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessAll}))
|
||||
assert.False(t, Rules.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessAll, AccessShared}))
|
||||
assert.True(t, Rules.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||
assert.False(t, Rules.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessAll}))
|
||||
})
|
||||
t.Run("ResourcePhotosRoleAdminActionModify", func(t *testing.T) {
|
||||
assert.True(t, Resources.AllowAll(ResourcePhotos, RoleAdmin, Permissions{ActionUpdate}))
|
||||
assert.True(t, Rules.AllowAll(ResourcePhotos, RoleAdmin, Permissions{ActionUpdate}))
|
||||
})
|
||||
t.Run("ResourceDefaultRoleAdminActionDefault", func(t *testing.T) {
|
||||
assert.True(t, Resources.AllowAll(ResourceDefault, RoleAdmin, Permissions{FullAccess}))
|
||||
assert.True(t, Rules.AllowAll(ResourceDefault, RoleAdmin, Permissions{FullAccess}))
|
||||
})
|
||||
t.Run("ResourceDefaultRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.AllowAll(ResourceDefault, RoleVisitor, Permissions{FullAccess}))
|
||||
assert.False(t, Rules.AllowAll(ResourceDefault, RoleVisitor, Permissions{FullAccess}))
|
||||
})
|
||||
t.Run("ResourcePhotosRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.AllowAll(ResourcePhotos, RoleVisitor, Permissions{FullAccess}))
|
||||
assert.False(t, Rules.AllowAll(ResourcePhotos, RoleVisitor, Permissions{FullAccess}))
|
||||
})
|
||||
t.Run("ResourceAlbumsRoleVisitorAccessShared", func(t *testing.T) {
|
||||
assert.True(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||
assert.True(t, Rules.AllowAll(ResourceAlbums, RoleVisitor, Permissions{AccessShared}))
|
||||
})
|
||||
t.Run("ResourceAlbumsRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{FullAccess}))
|
||||
assert.False(t, Rules.AllowAll(ResourceAlbums, RoleVisitor, Permissions{FullAccess}))
|
||||
})
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
assert.False(t, Resources.AllowAll(ResourceAlbums, RoleVisitor, Permissions{}))
|
||||
assert.False(t, Rules.AllowAll(ResourceAlbums, RoleVisitor, Permissions{}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestACL_Deny(t *testing.T) {
|
||||
t.Run("ResourceDefaultRoleAdminActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.Deny(ResourceDefault, RoleAdmin, FullAccess))
|
||||
assert.False(t, Rules.Deny(ResourceDefault, RoleAdmin, FullAccess))
|
||||
})
|
||||
t.Run("ResourceDefaultRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.True(t, Resources.Deny(ResourceDefault, RoleVisitor, FullAccess))
|
||||
assert.True(t, Rules.Deny(ResourceDefault, RoleVisitor, FullAccess))
|
||||
})
|
||||
t.Run("ResourceAlbumsRoleVisitorActionAccessShared", func(t *testing.T) {
|
||||
assert.False(t, Resources.Deny(ResourceAlbums, RoleVisitor, AccessShared))
|
||||
assert.False(t, Rules.Deny(ResourceAlbums, RoleVisitor, AccessShared))
|
||||
})
|
||||
t.Run("ResourcePhotosRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.True(t, Resources.Deny(ResourcePhotos, RoleVisitor, FullAccess))
|
||||
assert.True(t, Rules.Deny(ResourcePhotos, RoleVisitor, FullAccess))
|
||||
})
|
||||
t.Run("ResourceAlbumsRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.True(t, Resources.Deny(ResourceAlbums, RoleVisitor, FullAccess))
|
||||
assert.True(t, Rules.Deny(ResourceAlbums, RoleVisitor, FullAccess))
|
||||
})
|
||||
}
|
||||
|
||||
func TestACL_DenyAll(t *testing.T) {
|
||||
t.Run("ResourceFilesRoleVisitorActionDefault", func(t *testing.T) {
|
||||
assert.True(t, Resources.DenyAll(ResourceFiles, RoleVisitor, Permissions{FullAccess, AccessShared, ActionView}))
|
||||
assert.True(t, Rules.DenyAll(ResourceFiles, RoleVisitor, Permissions{FullAccess, AccessShared, ActionView}))
|
||||
})
|
||||
t.Run("ResourceFilesRoleAdminActionDefault", func(t *testing.T) {
|
||||
assert.False(t, Resources.DenyAll(ResourceFiles, RoleAdmin, Permissions{FullAccess, AccessShared, ActionView}))
|
||||
assert.False(t, Rules.DenyAll(ResourceFiles, RoleAdmin, Permissions{FullAccess, AccessShared, ActionView}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestACL_Resources(t *testing.T) {
|
||||
t.Run("Resources", func(t *testing.T) {
|
||||
result := Resources.Resources()
|
||||
t.Run("Rules", func(t *testing.T) {
|
||||
result := Rules.Resources()
|
||||
assert.Len(t, result, len(ResourceNames)-1)
|
||||
})
|
||||
}
|
||||
|
||||
84
internal/acl/const.go
Normal file
84
internal/acl/const.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package acl
|
||||
|
||||
// Roles that can be granted Permissions to use a Resource.
|
||||
const (
|
||||
RoleDefault Role = "default"
|
||||
RoleAdmin Role = "admin"
|
||||
RoleVisitor Role = "visitor"
|
||||
RoleClient Role = "client"
|
||||
RoleNone Role = ""
|
||||
)
|
||||
|
||||
// Permissions to use a Resource that can be granted to a Role.
|
||||
const (
|
||||
FullAccess Permission = "full_access"
|
||||
AccessShared Permission = "access_shared"
|
||||
AccessLibrary Permission = "access_library"
|
||||
AccessPrivate Permission = "access_private"
|
||||
AccessOwn Permission = "access_own"
|
||||
AccessAll Permission = "access_all"
|
||||
ActionSearch Permission = "search"
|
||||
ActionView Permission = "view"
|
||||
ActionUpload Permission = "upload"
|
||||
ActionCreate Permission = "create"
|
||||
ActionUpdate Permission = "update"
|
||||
ActionDownload Permission = "download"
|
||||
ActionShare Permission = "share"
|
||||
ActionDelete Permission = "delete"
|
||||
ActionRate Permission = "rate"
|
||||
ActionReact Permission = "react"
|
||||
ActionSubscribe Permission = "subscribe"
|
||||
ActionManage Permission = "manage"
|
||||
ActionManageOwn Permission = "manage_own"
|
||||
)
|
||||
|
||||
// A Role can be given Permission to use a Resource.
|
||||
const (
|
||||
ResourceFiles Resource = "files"
|
||||
ResourceFolders Resource = "folders"
|
||||
ResourceShares Resource = "shares"
|
||||
ResourcePhotos Resource = "photos"
|
||||
ResourceVideos Resource = "videos"
|
||||
ResourceFavorites Resource = "favorites"
|
||||
ResourceAlbums Resource = "albums"
|
||||
ResourceMoments Resource = "moments"
|
||||
ResourceCalendar Resource = "calendar"
|
||||
ResourcePeople Resource = "people"
|
||||
ResourcePlaces Resource = "places"
|
||||
ResourceLabels Resource = "labels"
|
||||
ResourceConfig Resource = "config"
|
||||
ResourceSettings Resource = "settings"
|
||||
ResourcePasscode Resource = "passcode"
|
||||
ResourcePassword Resource = "password"
|
||||
ResourceServices Resource = "services"
|
||||
ResourceUsers Resource = "users"
|
||||
ResourceSessions Resource = "sessions"
|
||||
ResourceLogs Resource = "logs"
|
||||
ResourceWebDAV Resource = "webdav"
|
||||
ResourceMetrics Resource = "metrics"
|
||||
ResourceFeedback Resource = "feedback"
|
||||
ResourceDefault Resource = "default"
|
||||
)
|
||||
|
||||
// Events for which a Role can be granted the ActionSubscribe Permission.
|
||||
const (
|
||||
ChannelUser Resource = "user"
|
||||
ChannelSession Resource = "session"
|
||||
ChannelAudit Resource = "audit"
|
||||
ChannelLog Resource = "log"
|
||||
ChannelNotify Resource = "notify"
|
||||
ChannelIndex Resource = "index"
|
||||
ChannelUpload Resource = "upload"
|
||||
ChannelImport Resource = "import"
|
||||
ChannelConfig Resource = "config"
|
||||
ChannelCount Resource = "count"
|
||||
ChannelPhotos Resource = "photos"
|
||||
ChannelCameras Resource = "cameras"
|
||||
ChannelLenses Resource = "lenses"
|
||||
ChannelCountries Resource = "countries"
|
||||
ChannelAlbums Resource = "albums"
|
||||
ChannelLabels Resource = "labels"
|
||||
ChannelSubjects Resource = "subjects"
|
||||
ChannelPeople Resource = "people"
|
||||
ChannelSync Resource = "sync"
|
||||
)
|
||||
@@ -11,6 +11,7 @@ var (
|
||||
AccessOwn: true,
|
||||
AccessShared: true,
|
||||
AccessLibrary: true,
|
||||
ActionView: true,
|
||||
ActionCreate: true,
|
||||
ActionUpdate: true,
|
||||
ActionDelete: true,
|
||||
@@ -21,22 +22,66 @@ var (
|
||||
ActionManage: true,
|
||||
ActionSubscribe: true,
|
||||
}
|
||||
GrantSubscribeAll = Grant{
|
||||
AccessAll: true,
|
||||
ActionSubscribe: true,
|
||||
}
|
||||
GrantSubscribeOwn = Grant{
|
||||
GrantOwn = Grant{
|
||||
AccessOwn: true,
|
||||
ActionView: true,
|
||||
ActionCreate: true,
|
||||
ActionUpdate: true,
|
||||
ActionDelete: true,
|
||||
ActionSubscribe: true,
|
||||
}
|
||||
GrantViewAll = Grant{
|
||||
AccessAll: true,
|
||||
ActionView: true,
|
||||
GrantAll = Grant{
|
||||
AccessAll: true,
|
||||
AccessOwn: true,
|
||||
ActionView: true,
|
||||
ActionCreate: true,
|
||||
ActionUpdate: true,
|
||||
ActionDelete: true,
|
||||
ActionSubscribe: true,
|
||||
}
|
||||
GrantManageOwn = Grant{
|
||||
AccessOwn: true,
|
||||
ActionView: true,
|
||||
ActionCreate: true,
|
||||
ActionUpdate: true,
|
||||
ActionDelete: true,
|
||||
ActionSubscribe: true,
|
||||
ActionManageOwn: true,
|
||||
}
|
||||
GrantConfigureOwn = Grant{
|
||||
AccessOwn: true,
|
||||
ActionCreate: true,
|
||||
ActionUpdate: true,
|
||||
ActionDelete: true,
|
||||
}
|
||||
GrantUpdateOwn = Grant{
|
||||
AccessOwn: true,
|
||||
ActionUpdate: true,
|
||||
}
|
||||
GrantViewOwn = Grant{
|
||||
AccessOwn: true,
|
||||
ActionView: true,
|
||||
}
|
||||
GrantViewUpdateOwn = Grant{
|
||||
AccessOwn: true,
|
||||
ActionView: true,
|
||||
ActionUpdate: true,
|
||||
}
|
||||
GrantViewLibrary = Grant{
|
||||
AccessLibrary: true,
|
||||
ActionView: true,
|
||||
}
|
||||
GrantViewAll = Grant{
|
||||
AccessAll: true,
|
||||
AccessOwn: true,
|
||||
ActionView: true,
|
||||
}
|
||||
GrantViewUpdateAll = Grant{
|
||||
AccessAll: true,
|
||||
AccessOwn: true,
|
||||
ActionView: true,
|
||||
ActionUpdate: true,
|
||||
}
|
||||
GrantViewShared = Grant{
|
||||
AccessShared: true,
|
||||
ActionView: true,
|
||||
@@ -48,10 +93,30 @@ var (
|
||||
ActionView: true,
|
||||
ActionDownload: true,
|
||||
}
|
||||
GrantSearchAll = Grant{
|
||||
AccessAll: true,
|
||||
ActionView: true,
|
||||
ActionSearch: true,
|
||||
}
|
||||
GrantSubscribeOwn = Grant{
|
||||
AccessOwn: true,
|
||||
ActionSubscribe: true,
|
||||
}
|
||||
GrantSubscribeAll = Grant{
|
||||
AccessAll: true,
|
||||
ActionSubscribe: true,
|
||||
}
|
||||
GrantNone = Grant{}
|
||||
)
|
||||
|
||||
// Allow checks whether the permission is granted.
|
||||
// GrantDefaults defines default grants for all supported roles.
|
||||
var GrantDefaults = Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
RoleVisitor: GrantViewShared,
|
||||
RoleClient: GrantFullAccess,
|
||||
}
|
||||
|
||||
// Allow checks if this Grant includes the specified Permission.
|
||||
func (grant Grant) Allow(perm Permission) bool {
|
||||
if result, ok := grant[perm]; ok {
|
||||
return result
|
||||
@@ -62,9 +127,13 @@ func (grant Grant) Allow(perm Permission) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GrantDefaults defines default grants for all supported roles.
|
||||
var GrantDefaults = Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
RoleVisitor: GrantViewShared,
|
||||
RoleClient: GrantFullAccess,
|
||||
// DenyAny checks if any of the Permissions are not covered by this Grant.
|
||||
func (grant Grant) DenyAny(perms Permissions) bool {
|
||||
for i := range perms {
|
||||
if !grant.Allow(perms[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -47,3 +47,45 @@ func TestGrant_Allow(t *testing.T) {
|
||||
assert.True(t, GrantViewOwn.Allow(AccessOwn))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGrant_DenyAny(t *testing.T) {
|
||||
t.Run("GrantFullAccessAll", func(t *testing.T) {
|
||||
assert.False(t, GrantFullAccess.DenyAny(Permissions{AccessAll}))
|
||||
})
|
||||
t.Run("GrantFullAccessDownload", func(t *testing.T) {
|
||||
assert.False(t, GrantFullAccess.DenyAny(Permissions{ActionDownload}))
|
||||
})
|
||||
t.Run("GrantFullAccessDelete", func(t *testing.T) {
|
||||
assert.False(t, GrantFullAccess.DenyAny(Permissions{ActionDelete}))
|
||||
})
|
||||
t.Run("GrantFullAccessLibrary", func(t *testing.T) {
|
||||
assert.False(t, GrantFullAccess.DenyAny(Permissions{AccessLibrary}))
|
||||
})
|
||||
t.Run("UnknownAction", func(t *testing.T) {
|
||||
assert.True(t, GrantViewAll.DenyAny(Permissions{"lovecats"}))
|
||||
})
|
||||
t.Run("ViewAllView", func(t *testing.T) {
|
||||
assert.False(t, GrantViewAll.DenyAny(Permissions{ActionView}))
|
||||
})
|
||||
t.Run("ViewAllAccessAll", func(t *testing.T) {
|
||||
assert.False(t, GrantViewAll.DenyAny(Permissions{AccessAll}))
|
||||
})
|
||||
t.Run("ViewAllDownload", func(t *testing.T) {
|
||||
assert.True(t, GrantViewAll.DenyAny(Permissions{ActionDownload}))
|
||||
})
|
||||
t.Run("ViewAllShare", func(t *testing.T) {
|
||||
assert.True(t, GrantViewAll.DenyAny(Permissions{ActionShare}))
|
||||
})
|
||||
t.Run("ViewOwnShare", func(t *testing.T) {
|
||||
assert.True(t, GrantViewOwn.DenyAny(Permissions{ActionShare}))
|
||||
})
|
||||
t.Run("ViewOwnView", func(t *testing.T) {
|
||||
assert.False(t, GrantViewOwn.DenyAny(Permissions{ActionView}))
|
||||
})
|
||||
t.Run("ViewOwnAccessAll", func(t *testing.T) {
|
||||
assert.True(t, GrantViewOwn.DenyAny(Permissions{AccessAll}))
|
||||
})
|
||||
t.Run("ViewOwnAccessOwn", func(t *testing.T) {
|
||||
assert.False(t, GrantViewOwn.DenyAny(Permissions{AccessOwn}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
|
||||
func TestACL_Grants(t *testing.T) {
|
||||
t.Run("RoleAdmin", func(t *testing.T) {
|
||||
result := Resources.Grants(RoleAdmin)
|
||||
result := Rules.Grants(RoleAdmin)
|
||||
assert.True(t, result[ResourcePhotos][ActionManage])
|
||||
assert.True(t, result[ResourceConfig][ActionManage])
|
||||
})
|
||||
t.Run("RoleVisitor", func(t *testing.T) {
|
||||
result := Resources.Grants(RoleVisitor)
|
||||
result := Rules.Grants(RoleVisitor)
|
||||
assert.False(t, result[ResourcePhotos][ActionUpdate])
|
||||
assert.False(t, result[ResourceConfig][ActionManage])
|
||||
})
|
||||
|
||||
@@ -5,28 +5,6 @@ import "strings"
|
||||
// Permission represents a single ability.
|
||||
type Permission string
|
||||
|
||||
// Permissions that can be granted to roles.
|
||||
const (
|
||||
FullAccess Permission = "full_access"
|
||||
AccessShared Permission = "access_shared"
|
||||
AccessLibrary Permission = "access_library"
|
||||
AccessPrivate Permission = "access_private"
|
||||
AccessOwn Permission = "access_own"
|
||||
AccessAll Permission = "access_all"
|
||||
ActionSearch Permission = "search"
|
||||
ActionView Permission = "view"
|
||||
ActionUpload Permission = "upload"
|
||||
ActionCreate Permission = "create"
|
||||
ActionUpdate Permission = "update"
|
||||
ActionDownload Permission = "download"
|
||||
ActionShare Permission = "share"
|
||||
ActionDelete Permission = "delete"
|
||||
ActionRate Permission = "rate"
|
||||
ActionReact Permission = "react"
|
||||
ActionManage Permission = "manage"
|
||||
ActionSubscribe Permission = "subscribe"
|
||||
)
|
||||
|
||||
// String returns the type as string.
|
||||
func (p Permission) String() string {
|
||||
return strings.ReplaceAll(string(p), "_", " ")
|
||||
@@ -46,17 +24,3 @@ func (p Permission) Equal(s string) bool {
|
||||
func (p Permission) NotEqual(s string) bool {
|
||||
return !p.Equal(s)
|
||||
}
|
||||
|
||||
// Permissions is a list of permissions.
|
||||
type Permissions []Permission
|
||||
|
||||
// String returns the permissions as a comma-separated string.
|
||||
func (perm Permissions) String() string {
|
||||
s := make([]string, len(perm))
|
||||
|
||||
for i := range perm {
|
||||
s[i] = perm[i].String()
|
||||
}
|
||||
|
||||
return strings.Join(s, ", ")
|
||||
}
|
||||
|
||||
17
internal/acl/permissions.go
Normal file
17
internal/acl/permissions.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package acl
|
||||
|
||||
import "strings"
|
||||
|
||||
// Permissions represents a list of permissions.
|
||||
type Permissions []Permission
|
||||
|
||||
// String returns the permissions as a comma-separated string.
|
||||
func (perm Permissions) String() string {
|
||||
s := make([]string, len(perm))
|
||||
|
||||
for i := range perm {
|
||||
s[i] = perm[i].String()
|
||||
}
|
||||
|
||||
return strings.Join(s, ", ")
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package acl
|
||||
|
||||
// Events that Roles can be granted Permission to listen to.
|
||||
const (
|
||||
ChannelUser Resource = "user"
|
||||
ChannelSession Resource = "session"
|
||||
ChannelAudit Resource = "audit"
|
||||
ChannelLog Resource = "log"
|
||||
ChannelNotify Resource = "notify"
|
||||
ChannelIndex Resource = "index"
|
||||
ChannelUpload Resource = "upload"
|
||||
ChannelImport Resource = "import"
|
||||
ChannelConfig Resource = "config"
|
||||
ChannelCount Resource = "count"
|
||||
ChannelPhotos Resource = "photos"
|
||||
ChannelCameras Resource = "cameras"
|
||||
ChannelLenses Resource = "lenses"
|
||||
ChannelCountries Resource = "countries"
|
||||
ChannelAlbums Resource = "albums"
|
||||
ChannelLabels Resource = "labels"
|
||||
ChannelSubjects Resource = "subjects"
|
||||
ChannelPeople Resource = "people"
|
||||
ChannelSync Resource = "sync"
|
||||
)
|
||||
@@ -1,33 +1,5 @@
|
||||
package acl
|
||||
|
||||
// Resources that Roles can be granted Permission.
|
||||
const (
|
||||
ResourceFiles Resource = "files"
|
||||
ResourceFolders Resource = "folders"
|
||||
ResourceShares Resource = "shares"
|
||||
ResourcePhotos Resource = "photos"
|
||||
ResourceVideos Resource = "videos"
|
||||
ResourceFavorites Resource = "favorites"
|
||||
ResourceAlbums Resource = "albums"
|
||||
ResourceMoments Resource = "moments"
|
||||
ResourceCalendar Resource = "calendar"
|
||||
ResourcePeople Resource = "people"
|
||||
ResourcePlaces Resource = "places"
|
||||
ResourceLabels Resource = "labels"
|
||||
ResourceConfig Resource = "config"
|
||||
ResourceSettings Resource = "settings"
|
||||
ResourcePasscode Resource = "passcode"
|
||||
ResourcePassword Resource = "password"
|
||||
ResourceServices Resource = "services"
|
||||
ResourceUsers Resource = "users"
|
||||
ResourceSessions Resource = "sessions"
|
||||
ResourceLogs Resource = "logs"
|
||||
ResourceWebDAV Resource = "webdav"
|
||||
ResourceMetrics Resource = "metrics"
|
||||
ResourceFeedback Resource = "feedback"
|
||||
ResourceDefault Resource = "default"
|
||||
)
|
||||
|
||||
// ResourceNames contains a list of all specified resources.
|
||||
var ResourceNames = []Resource{
|
||||
ResourceFiles,
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
package acl
|
||||
|
||||
// Roles that can be assigned to users.
|
||||
const (
|
||||
RoleDefault Role = "default"
|
||||
RoleAdmin Role = "admin"
|
||||
RoleVisitor Role = "visitor"
|
||||
RoleClient Role = "client"
|
||||
RoleNone Role = ""
|
||||
)
|
||||
|
||||
// RoleStrings represents user role names mapped to roles.
|
||||
type RoleStrings = map[string]Role
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package acl
|
||||
|
||||
// Resources specifies granted permissions by Resource and Role.
|
||||
var Resources = ACL{
|
||||
// Rules specifies granted permissions by Resource and Role.
|
||||
var Rules = ACL{
|
||||
ResourceFiles: Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
RoleClient: GrantFullAccess,
|
||||
@@ -51,8 +51,8 @@ var Resources = ACL{
|
||||
},
|
||||
ResourceSettings: Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
RoleVisitor: Grant{AccessOwn: true, ActionView: true},
|
||||
RoleClient: Grant{AccessOwn: true, ActionView: true, ActionUpdate: true},
|
||||
RoleVisitor: GrantViewOwn,
|
||||
RoleClient: GrantViewUpdateOwn,
|
||||
},
|
||||
ResourceServices: Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
@@ -64,12 +64,12 @@ var Resources = ACL{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
},
|
||||
ResourceUsers: Roles{
|
||||
RoleAdmin: Grant{AccessAll: true, AccessOwn: true, ActionView: true, ActionCreate: true, ActionUpdate: true, ActionDelete: true, ActionSubscribe: true},
|
||||
RoleClient: Grant{AccessOwn: true, ActionView: true},
|
||||
RoleAdmin: GrantAll,
|
||||
RoleClient: GrantViewOwn,
|
||||
},
|
||||
ResourceSessions: Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
RoleDefault: Grant{AccessOwn: true, ActionView: true, ActionCreate: true, ActionUpdate: true, ActionDelete: true, ActionSubscribe: true},
|
||||
RoleDefault: GrantOwn,
|
||||
},
|
||||
ResourceLogs: Roles{
|
||||
RoleAdmin: GrantFullAccess,
|
||||
37
internal/acl/scopes.go
Normal file
37
internal/acl/scopes.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package acl
|
||||
|
||||
// Permission scopes to Grant multiple Permissions for a Resource.
|
||||
const (
|
||||
ScopeRead Permission = "read"
|
||||
ScopeWrite Permission = "write"
|
||||
)
|
||||
|
||||
var (
|
||||
GrantScopeRead = Grant{
|
||||
AccessShared: true,
|
||||
AccessLibrary: true,
|
||||
AccessPrivate: true,
|
||||
AccessOwn: true,
|
||||
AccessAll: true,
|
||||
ActionSearch: true,
|
||||
ActionView: true,
|
||||
ActionDownload: true,
|
||||
ActionSubscribe: true,
|
||||
}
|
||||
GrantScopeWrite = Grant{
|
||||
AccessShared: true,
|
||||
AccessLibrary: true,
|
||||
AccessPrivate: true,
|
||||
AccessOwn: true,
|
||||
AccessAll: true,
|
||||
ActionUpload: true,
|
||||
ActionCreate: true,
|
||||
ActionUpdate: true,
|
||||
ActionShare: true,
|
||||
ActionDelete: true,
|
||||
ActionRate: true,
|
||||
ActionReact: true,
|
||||
ActionManage: true,
|
||||
ActionManageOwn: true,
|
||||
}
|
||||
)
|
||||
37
internal/acl/scopes_test.go
Normal file
37
internal/acl/scopes_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package acl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGrantScopeRead(t *testing.T) {
|
||||
t.Run("ActionView", func(t *testing.T) {
|
||||
assert.True(t, GrantScopeRead.Allow(ActionView))
|
||||
assert.False(t, GrantScopeRead.DenyAny(Permissions{ActionView}))
|
||||
})
|
||||
t.Run("ActionUpdate", func(t *testing.T) {
|
||||
assert.False(t, GrantScopeRead.Allow(ActionUpdate))
|
||||
assert.True(t, GrantScopeRead.DenyAny(Permissions{ActionUpdate}))
|
||||
})
|
||||
t.Run("AccessAll", func(t *testing.T) {
|
||||
assert.True(t, GrantScopeRead.Allow(AccessAll))
|
||||
assert.False(t, GrantScopeRead.DenyAny(Permissions{AccessAll}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGrantScopeWrite(t *testing.T) {
|
||||
t.Run("ActionView", func(t *testing.T) {
|
||||
assert.False(t, GrantScopeWrite.Allow(ActionView))
|
||||
assert.True(t, GrantScopeWrite.DenyAny(Permissions{ActionView}))
|
||||
})
|
||||
t.Run("ActionUpdate", func(t *testing.T) {
|
||||
assert.True(t, GrantScopeWrite.Allow(ActionUpdate))
|
||||
assert.False(t, GrantScopeWrite.DenyAny(Permissions{ActionUpdate}))
|
||||
})
|
||||
t.Run("AccessAll", func(t *testing.T) {
|
||||
assert.True(t, GrantScopeWrite.Allow(AccessAll))
|
||||
assert.False(t, GrantScopeWrite.DenyAny(Permissions{AccessAll}))
|
||||
})
|
||||
}
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
|
||||
// Auth checks if the user is authorized to access a resource with the given permission
|
||||
// and returns the session or nil otherwise.
|
||||
func Auth(c *gin.Context, resource acl.Resource, grant acl.Permission) *entity.Session {
|
||||
return AuthAny(c, resource, acl.Permissions{grant})
|
||||
func Auth(c *gin.Context, resource acl.Resource, perm acl.Permission) *entity.Session {
|
||||
return AuthAny(c, resource, acl.Permissions{perm})
|
||||
}
|
||||
|
||||
// AuthAny checks if the user is authorized to access a resource with any of the specified permissions
|
||||
// and returns the session or nil otherwise.
|
||||
func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *entity.Session) {
|
||||
func AuthAny(c *gin.Context, resource acl.Resource, perms acl.Permissions) (s *entity.Session) {
|
||||
// Prevent CDNs from caching responses that require authentication.
|
||||
if header.IsCdn(c.Request) {
|
||||
return entity.SessionStatusForbidden()
|
||||
@@ -30,7 +30,7 @@ func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *
|
||||
|
||||
// Find active session to perform authorization check or deny if no session was found.
|
||||
if s = Session(clientIp, authToken); s == nil {
|
||||
event.AuditWarn([]string{clientIp, "%s %s without authentication", "denied"}, grants.String(), string(resource))
|
||||
event.AuditWarn([]string{clientIp, "%s %s without authentication", "denied"}, perms.String(), string(resource))
|
||||
return entity.SessionStatusUnauthorized()
|
||||
}
|
||||
|
||||
@@ -41,33 +41,33 @@ func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *
|
||||
// If the request is from a client application, check its authorization based
|
||||
// on the allowed scope, the ACL, and the user account it belongs to (if any).
|
||||
if s.IsClient() {
|
||||
// Check ACL resource name against the permitted scope.
|
||||
if !s.HasScope(resource.String()) {
|
||||
// Check the resource and required permissions against the session scope.
|
||||
if s.ScopeExcludes(resource, perms) {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "access %s", "denied"}, clean.Log(s.ClientInfo()), s.RefID, string(resource))
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
// Perform an authorization check based on the ACL defaults for client applications.
|
||||
if acl.Resources.DenyAll(resource, s.ClientRole(), grants) {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s", "denied"}, clean.Log(s.ClientInfo()), s.RefID, grants.String(), string(resource))
|
||||
// Check request authorization against client application ACL rules.
|
||||
if acl.Rules.DenyAll(resource, s.ClientRole(), perms) {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s", "denied"}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource))
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
// Additionally check the user authorization if the client belongs to a user account.
|
||||
// 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", "granted"}, clean.Log(s.ClientInfo()), s.RefID, grants.String(), string(resource))
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s", "granted"}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource))
|
||||
} else if u := s.User(); !u.IsDisabled() && !u.IsUnknown() && u.IsRegistered() {
|
||||
if acl.Resources.DenyAll(resource, u.AclRole(), grants) {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as %s", "denied"}, clean.Log(s.ClientInfo()), s.RefID, grants.String(), string(resource), u.String())
|
||||
if acl.Rules.DenyAll(resource, u.AclRole(), perms) {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as %s", "denied"}, clean.Log(s.ClientInfo()), 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", "granted"}, clean.Log(s.ClientInfo()), s.RefID, grants.String(), string(resource), u.String())
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s as %s", "granted"}, clean.Log(s.ClientInfo()), 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", "denied"}, clean.Log(s.ClientInfo()), s.RefID, grants.String(), string(resource))
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s as unauthorized user", "denied"}, clean.Log(s.ClientInfo()), s.RefID, perms.String(), string(resource))
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
@@ -76,13 +76,13 @@ func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *
|
||||
|
||||
// Otherwise, perform a regular ACL authorization check based on the user role.
|
||||
if u := s.User(); u.IsUnknown() || u.IsDisabled() {
|
||||
event.AuditWarn([]string{clientIp, "session %s", "%s %s as unauthorized user", "denied"}, s.RefID, grants.String(), string(resource))
|
||||
event.AuditWarn([]string{clientIp, "session %s", "%s %s as unauthorized user", "denied"}, s.RefID, perms.String(), string(resource))
|
||||
return entity.SessionStatusUnauthorized()
|
||||
} else if acl.Resources.DenyAll(resource, u.AclRole(), grants) {
|
||||
event.AuditErr([]string{clientIp, "session %s", "%s %s as %s", "denied"}, s.RefID, grants.String(), string(resource), u.AclRole().String())
|
||||
} else if acl.Rules.DenyAll(resource, u.AclRole(), perms) {
|
||||
event.AuditErr([]string{clientIp, "session %s", "%s %s as %s", "denied"}, s.RefID, perms.String(), string(resource), u.AclRole().String())
|
||||
return entity.SessionStatusForbidden()
|
||||
} else {
|
||||
event.AuditInfo([]string{clientIp, "session %s", "%s %s as %s", "granted"}, s.RefID, grants.String(), string(resource), u.AclRole().String())
|
||||
event.AuditInfo([]string{clientIp, "session %s", "%s %s as %s", "granted"}, s.RefID, perms.String(), string(resource), u.AclRole().String())
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
|
||||
var err error
|
||||
|
||||
// Abort if user wants to delete all but does not have sufficient privileges.
|
||||
if f.All && !acl.Resources.AllowAll(acl.ResourcePhotos, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
if f.All && !acl.Rules.AllowAll(acl.ResourcePhotos, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -91,8 +91,8 @@ func SaveSettings(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
if acl.Resources.DenyAll(acl.ResourceSettings, s.UserRole(), acl.Permissions{acl.ActionUpdate, acl.ActionManage}) {
|
||||
c.JSON(http.StatusOK, user.Settings().Apply(settings).ApplyTo(conf.Settings().ApplyACL(acl.Resources, user.AclRole())))
|
||||
if acl.Rules.DenyAll(acl.ResourceSettings, s.UserRole(), acl.Permissions{acl.ActionUpdate, acl.ActionManage}) {
|
||||
c.JSON(http.StatusOK, user.Settings().Apply(settings).ApplyTo(conf.Settings().ApplyACL(acl.Rules, user.AclRole())))
|
||||
return
|
||||
} else if err := user.Settings().Apply(settings).Save(); err != nil {
|
||||
log.Debugf("config: %s (save user settings)", err)
|
||||
|
||||
@@ -68,7 +68,7 @@ func SearchFolders(router *gin.RouterGroup, urlPath, rootName, rootPath string)
|
||||
// Exclude private content?
|
||||
if !get.Config().Settings().Features.Private {
|
||||
f.Public = false
|
||||
} else if acl.Resources.Deny(acl.ResourcePhotos, aclRole, acl.AccessPrivate) {
|
||||
} else if acl.Rules.Deny(acl.ResourcePhotos, aclRole, acl.AccessPrivate) {
|
||||
f.Public = true
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ func StartImport(router *gin.RouterGroup) {
|
||||
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", "granted"}, s.RefID, clean.Log(srcFolder), s.UserRole().String())
|
||||
} else if acl.Resources.Deny(acl.ResourceFiles, s.UserRole(), acl.ActionManage) {
|
||||
} else if acl.Rules.Deny(acl.ResourceFiles, s.UserRole(), acl.ActionManage) {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "import files from %s as %s", "denied"}, s.RefID, clean.Log(srcFolder), s.UserRole().String())
|
||||
AbortForbidden(c)
|
||||
return
|
||||
@@ -99,7 +99,7 @@ func StartImport(router *gin.RouterGroup) {
|
||||
|
||||
// Add imported files to albums if allowed.
|
||||
if len(f.Albums) > 0 &&
|
||||
acl.Resources.AllowAny(acl.ResourceAlbums, s.UserRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
||||
acl.Rules.AllowAny(acl.ResourceAlbums, s.UserRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
||||
log.Debugf("import: adding files to album %s", clean.Log(strings.Join(f.Albums, " and ")))
|
||||
opt.Albums = f.Albums
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func SearchPhotos(router *gin.RouterGroup) {
|
||||
// Ignore private flag if feature is disabled.
|
||||
if f.Scope == "" &&
|
||||
settings.Features.Review &&
|
||||
acl.Resources.Deny(acl.ResourcePhotos, s.UserRole(), acl.ActionManage) {
|
||||
acl.Rules.Deny(acl.ResourcePhotos, s.UserRole(), acl.ActionManage) {
|
||||
f.Quality = 3
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ func SearchGeo(router *gin.RouterGroup) {
|
||||
// Ignore private flag if feature is disabled.
|
||||
if f.Scope == "" &&
|
||||
settings.Features.Review &&
|
||||
acl.Resources.Deny(acl.ResourcePhotos, s.UserRole(), acl.ActionManage) {
|
||||
acl.Rules.Deny(acl.ResourcePhotos, s.UserRole(), acl.ActionManage) {
|
||||
f.Quality = 3
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,11 @@ func LikePhoto(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
if get.Config().Experimental() && acl.Resources.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionReact) {
|
||||
if get.Config().Experimental() && acl.Rules.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionReact) {
|
||||
logWarn("react", m.React(s.User(), react.Find("love")))
|
||||
}
|
||||
|
||||
if acl.Resources.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionUpdate) {
|
||||
if acl.Rules.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionUpdate) {
|
||||
err = m.SetFavorite(true)
|
||||
|
||||
if err != nil {
|
||||
@@ -71,11 +71,11 @@ func DislikePhoto(router *gin.RouterGroup) {
|
||||
return
|
||||
}
|
||||
|
||||
if get.Config().Experimental() && acl.Resources.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionReact) {
|
||||
if get.Config().Experimental() && acl.Rules.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionReact) {
|
||||
logWarn("react", m.UnReact(s.User()))
|
||||
}
|
||||
|
||||
if acl.Resources.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionUpdate) {
|
||||
if acl.Rules.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionUpdate) {
|
||||
err = m.SetFavorite(false)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -49,7 +49,7 @@ func DeleteSession(router *gin.RouterGroup) {
|
||||
|
||||
// Only admins may delete other sessions by ref id.
|
||||
if rnd.IsRefID(id) {
|
||||
if !acl.Resources.AllowAll(acl.ResourceSessions, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
if !acl.Rules.AllowAll(acl.ResourceSessions, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
event.AuditErr([]string{clientIp, "session %s", "delete %s as %s", "denied"}, s.RefID, acl.ResourceSessions.String(), s.UserRole())
|
||||
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||
return
|
||||
|
||||
@@ -36,7 +36,7 @@ func UploadUserAvatar(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
// Check if the session user is has user management privileges.
|
||||
isAdmin := acl.Resources.AllowAll(acl.ResourceUsers, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
isAdmin := acl.Rules.AllowAll(acl.ResourceUsers, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
uid := clean.UID(c.Param("uid"))
|
||||
|
||||
// Users may only change their own avatar.
|
||||
|
||||
@@ -43,7 +43,7 @@ func UpdateUserPassword(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
// Check if the current user has management privileges.
|
||||
isAdmin := acl.Resources.AllowAll(acl.ResourceUsers, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
isAdmin := acl.Rules.AllowAll(acl.ResourceUsers, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
isSuperAdmin := isAdmin && s.User().IsSuperAdmin()
|
||||
uid := clean.UID(c.Param("uid"))
|
||||
|
||||
|
||||
67
internal/api/users_sessions.go
Normal file
67
internal/api/users_sessions.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/search"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// FindUserSessions finds user sessions and returns them as JSON.
|
||||
//
|
||||
// GET /api/v1/users/:uid/sessions
|
||||
func FindUserSessions(router *gin.RouterGroup) {
|
||||
router.GET("/users/:uid/sessions", func(c *gin.Context) {
|
||||
// Check if the session user is has user management privileges.
|
||||
s := Auth(c, acl.ResourceSessions, acl.ActionManageOwn)
|
||||
|
||||
if s.Abort(c) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get global config.
|
||||
conf := get.Config()
|
||||
|
||||
// Check feature flags and authorization.
|
||||
if conf.Public() || conf.Demo() || !s.HasRegisteredUser() || conf.DisableSettings() {
|
||||
c.JSON(http.StatusNotFound, entity.Users{})
|
||||
return
|
||||
} else if !rnd.IsUID(s.UserUID, entity.UserUID) || s.UserUID != c.Param("uid") {
|
||||
c.JSON(http.StatusForbidden, entity.Users{})
|
||||
return
|
||||
}
|
||||
|
||||
var f form.SearchSessions
|
||||
|
||||
// Init search form.
|
||||
err := c.MustBindWith(&f, binding.Form)
|
||||
|
||||
if err != nil {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
// Filter by user.
|
||||
f.UID = s.UserUID
|
||||
|
||||
// Perform search.
|
||||
result, err := search.Sessions(f)
|
||||
|
||||
if err != nil {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
}
|
||||
|
||||
AddLimitHeader(c, f.Count)
|
||||
AddOffsetHeader(c, f.Offset)
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
})
|
||||
}
|
||||
48
internal/api/users_sessions_test.go
Normal file
48
internal/api/users_sessions_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFindUserSessions(t *testing.T) {
|
||||
t.Run("Public", func(t *testing.T) {
|
||||
app, router, _ := NewApiTest()
|
||||
FindUserSessions(router)
|
||||
r := PerformRequest(app, "GET", "/api/v1/users/uqxc08w3d0ej2283/sessions")
|
||||
assert.Equal(t, http.StatusNotFound, r.Code)
|
||||
})
|
||||
t.Run("Unauthorized", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
FindUserSessions(router)
|
||||
|
||||
r := PerformRequest(app, "GET", "/api/v1/users/uqxc08w3d0ej2283/sessions")
|
||||
assert.Equal(t, http.StatusUnauthorized, r.Code)
|
||||
})
|
||||
t.Run("InvalidUID", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
FindUserSessions(router)
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
r := AuthenticatedRequest(app, "GET", "/api/v1/users/uqxetseacy5eo9z2/sessions?count=100", sessId)
|
||||
assert.Equal(t, http.StatusForbidden, r.Code)
|
||||
})
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
conf.SetAuthMode(config.AuthModePasswd)
|
||||
defer conf.SetAuthMode(config.AuthModePublic)
|
||||
FindUserSessions(router)
|
||||
sessId := AuthenticateUser(app, router, "alice", "Alice123!")
|
||||
|
||||
r := AuthenticatedRequest(app, "GET", "/api/v1/users/uqxetse3cy5eo9z2/sessions?count=100", sessId)
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
// t.Logf("FindUserSessions/Success: %s", r.Body.String())
|
||||
})
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func UpdateUser(router *gin.RouterGroup) {
|
||||
}
|
||||
|
||||
// Check if the session user is has user management privileges.
|
||||
isAdmin := acl.Resources.AllowAll(acl.ResourceUsers, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
isAdmin := acl.Rules.AllowAll(acl.ResourceUsers, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
privilegeLevelChange := isAdmin && m.PrivilegeLevelChange(f)
|
||||
|
||||
// Get user from session.
|
||||
|
||||
@@ -197,7 +197,7 @@ func ProcessUserUpload(router *gin.RouterGroup) {
|
||||
|
||||
// Add imported files to albums if allowed.
|
||||
if len(f.Albums) > 0 &&
|
||||
acl.Resources.AllowAny(acl.ResourceAlbums, s.UserRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
||||
acl.Rules.AllowAny(acl.ResourceAlbums, s.UserRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
||||
log.Debugf("upload: adding files to album %s", clean.Log(strings.Join(f.Albums, " and ")))
|
||||
opt.Albums = f.Albums
|
||||
}
|
||||
|
||||
@@ -221,14 +221,14 @@ func (c *Config) Flags() (flags []string) {
|
||||
// ClientPublic returns config values for use by the JavaScript UI and other clients.
|
||||
func (c *Config) ClientPublic() ClientConfig {
|
||||
if c.Public() {
|
||||
return c.ClientUser(true).ApplyACL(acl.Resources, acl.RoleAdmin)
|
||||
return c.ClientUser(true).ApplyACL(acl.Rules, acl.RoleAdmin)
|
||||
}
|
||||
|
||||
a := c.ClientAssets()
|
||||
|
||||
cfg := ClientConfig{
|
||||
Settings: c.PublicSettings(),
|
||||
ACL: acl.Resources.Grants(acl.RoleNone),
|
||||
ACL: acl.Rules.Grants(acl.RoleNone),
|
||||
Disable: ClientDisable{
|
||||
WebDAV: true,
|
||||
Settings: c.DisableSettings(),
|
||||
@@ -317,7 +317,7 @@ func (c *Config) ClientShare() ClientConfig {
|
||||
|
||||
cfg := ClientConfig{
|
||||
Settings: c.ShareSettings(),
|
||||
ACL: acl.Resources.Grants(acl.RoleVisitor),
|
||||
ACL: acl.Rules.Grants(acl.RoleVisitor),
|
||||
Disable: ClientDisable{
|
||||
WebDAV: c.DisableWebDAV(),
|
||||
Settings: c.DisableSettings(),
|
||||
@@ -677,18 +677,18 @@ func (c *Config) ClientUser(withSettings bool) ClientConfig {
|
||||
|
||||
// ClientRole provides the client config values for the specified user role.
|
||||
func (c *Config) ClientRole(role acl.Role) ClientConfig {
|
||||
return c.ClientUser(true).ApplyACL(acl.Resources, role)
|
||||
return c.ClientUser(true).ApplyACL(acl.Rules, role)
|
||||
}
|
||||
|
||||
// ClientSession provides the client config values for the specified session.
|
||||
func (c *Config) ClientSession(sess *entity.Session) (cfg ClientConfig) {
|
||||
if sess.NoUser() && sess.IsClient() {
|
||||
cfg = c.ClientUser(false).ApplyACL(acl.Resources, sess.ClientRole())
|
||||
cfg = c.ClientUser(false).ApplyACL(acl.Rules, sess.ClientRole())
|
||||
cfg.Settings = c.SessionSettings(sess)
|
||||
} else if sess.User().IsVisitor() {
|
||||
cfg = c.ClientShare()
|
||||
} else if sess.User().IsRegistered() {
|
||||
cfg = c.ClientUser(false).ApplyACL(acl.Resources, sess.UserRole())
|
||||
cfg = c.ClientUser(false).ApplyACL(acl.Rules, sess.UserRole())
|
||||
cfg.Settings = c.SessionSettings(sess)
|
||||
} else {
|
||||
cfg = c.ClientPublic()
|
||||
|
||||
@@ -73,7 +73,7 @@ func (c *Config) SessionSettings(sess *entity.Session) *customize.Settings {
|
||||
}
|
||||
|
||||
if sess.NoUser() && sess.IsClient() {
|
||||
return c.Settings().ApplyACL(acl.Resources, sess.ClientRole()).ApplyScope(sess.Scope())
|
||||
return c.Settings().ApplyACL(acl.Rules, sess.ClientRole()).ApplyScope(sess.Scope())
|
||||
}
|
||||
|
||||
user := sess.User()
|
||||
@@ -84,7 +84,7 @@ func (c *Config) SessionSettings(sess *entity.Session) *customize.Settings {
|
||||
}
|
||||
|
||||
// Apply role-based permissions and user settings to a copy of the global app settings.
|
||||
return user.Settings().ApplyTo(c.Settings().ApplyACL(acl.Resources, user.AclRole())).ApplyScope(sess.Scope())
|
||||
return user.Settings().ApplyTo(c.Settings().ApplyACL(acl.Rules, user.AclRole())).ApplyScope(sess.Scope())
|
||||
}
|
||||
|
||||
// PublicSettings returns the public app settings.
|
||||
@@ -102,7 +102,7 @@ func (c *Config) PublicSettings() *customize.Settings {
|
||||
|
||||
// ShareSettings returns the app settings for share link visitors.
|
||||
func (c *Config) ShareSettings() *customize.Settings {
|
||||
settings := c.Settings().ApplyACL(acl.Resources, acl.RoleVisitor)
|
||||
settings := c.Settings().ApplyACL(acl.Rules, acl.RoleVisitor)
|
||||
|
||||
return &customize.Settings{
|
||||
UI: settings.UI,
|
||||
|
||||
@@ -45,7 +45,7 @@ func TestSettings_ApplyACL(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.Equal(t, original, s.Features)
|
||||
r := s.ApplyACL(acl.Resources, acl.RoleAdmin)
|
||||
r := s.ApplyACL(acl.Rules, acl.RoleAdmin)
|
||||
|
||||
t.Logf("RoleAdmin: %#v", r)
|
||||
assert.Equal(t, expected, r.Features)
|
||||
@@ -85,7 +85,7 @@ func TestSettings_ApplyACL(t *testing.T) {
|
||||
}
|
||||
|
||||
assert.Equal(t, original, s.Features)
|
||||
r := s.ApplyACL(acl.Resources, acl.RoleVisitor)
|
||||
r := s.ApplyACL(acl.Rules, acl.RoleVisitor)
|
||||
t.Logf("RoleVisitor: %#v", r)
|
||||
assert.Equal(t, expected, r.Features)
|
||||
})
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
|
||||
func TestSettings_ApplyScope(t *testing.T) {
|
||||
original := NewDefaultSettings().Features
|
||||
admin := NewDefaultSettings().ApplyACL(acl.Resources, acl.RoleAdmin)
|
||||
client := NewDefaultSettings().ApplyACL(acl.Resources, acl.RoleClient)
|
||||
visitor := NewDefaultSettings().ApplyACL(acl.Resources, acl.RoleVisitor)
|
||||
admin := NewDefaultSettings().ApplyACL(acl.Rules, acl.RoleAdmin)
|
||||
client := NewDefaultSettings().ApplyACL(acl.Rules, acl.RoleClient)
|
||||
visitor := NewDefaultSettings().ApplyACL(acl.Rules, acl.RoleVisitor)
|
||||
|
||||
t.Run("AdminUnscoped", func(t *testing.T) {
|
||||
s := NewDefaultSettings()
|
||||
|
||||
@@ -518,9 +518,47 @@ func (m *Session) Scope() string {
|
||||
return clean.Scope(m.AuthScope)
|
||||
}
|
||||
|
||||
// HasScope checks if the session has the given authorization scope.
|
||||
func (m *Session) HasScope(scope string) bool {
|
||||
return list.ParseAttr(m.Scope()).Contains(scope)
|
||||
// ScopeAllows checks if the scope does not exclude access to specified resource.
|
||||
func (m *Session) ScopeAllows(resource acl.Resource, perms acl.Permissions) bool {
|
||||
// Get scope string.
|
||||
scope := m.Scope()
|
||||
|
||||
// Skip detailed check and allow all if scope is "*".
|
||||
if scope == list.All {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip resource check if scope includes all read operations.
|
||||
if scope == acl.ScopeRead.String() {
|
||||
return !acl.GrantScopeRead.DenyAny(perms)
|
||||
}
|
||||
|
||||
// Parse scope to check for resources and permissions.
|
||||
attr := list.ParseAttr(scope)
|
||||
|
||||
// Check if resource is within scope.
|
||||
if granted := attr.Contains(resource.String()); !granted {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if permission is within scope.
|
||||
if len(perms) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if scope is limited to read or write operations.
|
||||
if a := attr.Find(acl.ScopeRead.String()); a.Value == list.True && acl.GrantScopeRead.DenyAny(perms) {
|
||||
return false
|
||||
} else if a = attr.Find(acl.ScopeWrite.String()); a.Value == list.True && acl.GrantScopeWrite.DenyAny(perms) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// ScopeExcludes checks if the scope does not include access to specified resource.
|
||||
func (m *Session) ScopeExcludes(resource acl.Resource, perms acl.Permissions) bool {
|
||||
return !m.ScopeAllows(resource, perms)
|
||||
}
|
||||
|
||||
// SetScope sets a custom authentication scope.
|
||||
|
||||
@@ -127,7 +127,7 @@ func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (provider a
|
||||
event.LoginError(clientIp, "api", userName, m.UserAgent, message)
|
||||
m.Status = http.StatusUnauthorized
|
||||
return provider, method, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
} else if !authSess.IsClient() || !authSess.HasScope(acl.ResourceSessions.String()) {
|
||||
} else if !authSess.IsClient() || authSess.ScopeExcludes(acl.ResourceSessions, acl.Permissions{acl.ActionCreate}) {
|
||||
message := "unauthorized"
|
||||
limiter.Login.Reserve(clientIp)
|
||||
event.AuditErr([]string{clientIp, "session %s", "login as %s with app password", message}, m.RefID, clean.LogQuote(userName))
|
||||
|
||||
@@ -110,8 +110,8 @@ func TestAuthSession(t *testing.T) {
|
||||
assert.True(t, authSess.IsRegistered())
|
||||
assert.True(t, authSess.HasUser())
|
||||
|
||||
assert.True(t, authSess.HasScope(acl.ResourceWebDAV.String()))
|
||||
assert.True(t, authSess.HasScope(acl.ResourceSessions.String()))
|
||||
assert.True(t, authSess.ScopeAllows(acl.ResourceWebDAV, acl.Permissions{acl.ActionCreate}))
|
||||
assert.True(t, authSess.ScopeAllows(acl.ResourceSessions, acl.Permissions{acl.ActionCreate}))
|
||||
})
|
||||
t.Run("AliceTokenWebdav", func(t *testing.T) {
|
||||
s := SessionFixtures.Get("alice_token_webdav")
|
||||
@@ -148,8 +148,8 @@ func TestAuthSession(t *testing.T) {
|
||||
assert.True(t, authSess.IsRegistered())
|
||||
assert.True(t, authSess.HasUser())
|
||||
|
||||
assert.True(t, authSess.HasScope(acl.ResourceWebDAV.String()))
|
||||
assert.False(t, authSess.HasScope(acl.ResourceSessions.String()))
|
||||
assert.True(t, authSess.ScopeAllows(acl.ResourceWebDAV, acl.Permissions{acl.ActionCreate}))
|
||||
assert.False(t, authSess.ScopeAllows(acl.ResourceSessions, acl.Permissions{acl.ActionCreate}))
|
||||
})
|
||||
t.Run("EmptyPassword", func(t *testing.T) {
|
||||
// Create test request form.
|
||||
|
||||
@@ -544,6 +544,96 @@ func TestSession_SetAuthID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_ScopeAllows(t *testing.T) {
|
||||
t.Run("AnyScope", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
AuthScope: "*",
|
||||
}
|
||||
|
||||
assert.True(t, s.ScopeAllows("", nil))
|
||||
})
|
||||
t.Run("ReadScope", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
AuthScope: "read",
|
||||
}
|
||||
|
||||
assert.True(t, s.ScopeAllows("metrics", nil))
|
||||
assert.True(t, s.ScopeAllows("sessions", nil))
|
||||
assert.True(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionView, acl.AccessAll}))
|
||||
assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.False(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.False(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionCreate}))
|
||||
assert.False(t, s.ScopeAllows("sessions", acl.Permissions{acl.ActionDelete}))
|
||||
})
|
||||
t.Run("ReadAny", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
AuthScope: "read *",
|
||||
}
|
||||
|
||||
assert.True(t, s.ScopeAllows("metrics", nil))
|
||||
assert.True(t, s.ScopeAllows("sessions", nil))
|
||||
assert.True(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionView, acl.AccessAll}))
|
||||
assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.False(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.False(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionCreate}))
|
||||
assert.False(t, s.ScopeAllows("sessions", acl.Permissions{acl.ActionDelete}))
|
||||
})
|
||||
t.Run("ReadSettings", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
AuthScope: "read settings",
|
||||
}
|
||||
|
||||
assert.True(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionView}))
|
||||
assert.False(t, s.ScopeAllows("metrics", nil))
|
||||
assert.False(t, s.ScopeAllows("sessions", nil))
|
||||
assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionView, acl.AccessAll}))
|
||||
assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.False(t, s.ScopeAllows("metrics", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.False(t, s.ScopeAllows("settings", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.False(t, s.ScopeAllows("sessions", acl.Permissions{acl.ActionDelete}))
|
||||
assert.False(t, s.ScopeAllows("sessions", acl.Permissions{acl.ActionDelete}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_ScopeExcludes(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
AuthScope: "*",
|
||||
}
|
||||
|
||||
assert.False(t, s.ScopeExcludes("", nil))
|
||||
})
|
||||
t.Run("ReadSettings", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
AuthScope: "read settings",
|
||||
}
|
||||
|
||||
assert.False(t, s.ScopeExcludes("settings", acl.Permissions{acl.ActionView}))
|
||||
assert.True(t, s.ScopeExcludes("metrics", nil))
|
||||
assert.True(t, s.ScopeExcludes("sessions", nil))
|
||||
assert.True(t, s.ScopeExcludes("metrics", acl.Permissions{acl.ActionView, acl.AccessAll}))
|
||||
assert.True(t, s.ScopeExcludes("metrics", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.True(t, s.ScopeExcludes("metrics", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.True(t, s.ScopeExcludes("settings", acl.Permissions{acl.ActionUpdate}))
|
||||
assert.True(t, s.ScopeExcludes("sessions", acl.Permissions{acl.ActionDelete}))
|
||||
assert.True(t, s.ScopeExcludes("sessions", acl.Permissions{acl.ActionDelete}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_SetScope(t *testing.T) {
|
||||
t.Run("EmptyScope", func(t *testing.T) {
|
||||
s := &Session{
|
||||
|
||||
@@ -410,7 +410,7 @@ func (m *User) CanLogIn() bool {
|
||||
} else if m.IsDisabled() || m.IsUnknown() || !m.IsRegistered() {
|
||||
return false
|
||||
} else {
|
||||
return acl.Resources.Allow(acl.ResourceConfig, m.AclRole(), acl.AccessOwn)
|
||||
return acl.Rules.Allow(acl.ResourceConfig, m.AclRole(), acl.AccessOwn)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,7 +425,7 @@ func (m *User) CanUseWebDAV() bool {
|
||||
return false
|
||||
} else {
|
||||
// Check if the ACL allows downloading files via WebDAV based on the user role.
|
||||
return acl.Resources.Allow(acl.ResourceWebDAV, m.AclRole(), acl.ActionDownload)
|
||||
return acl.Rules.Allow(acl.ResourceWebDAV, m.AclRole(), acl.ActionDownload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@ func (m *User) CanUpload() bool {
|
||||
return false
|
||||
} else {
|
||||
// Check if the ACL allows uploading photos based on the user role.
|
||||
return acl.Resources.Allow(acl.ResourcePhotos, m.AclRole(), acl.ActionUpload)
|
||||
return acl.Rules.Allow(acl.ResourcePhotos, m.AclRole(), acl.ActionUpload)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -790,11 +790,11 @@ func (m *User) IsVisitor() bool {
|
||||
|
||||
// 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) {
|
||||
if acl.Rules.Deny(resource, m.AclRole(), acl.AccessShared) {
|
||||
return false
|
||||
}
|
||||
|
||||
return acl.Resources.DenyAll(resource, m.AclRole(), acl.Permissions{acl.AccessAll, acl.AccessLibrary})
|
||||
return acl.Rules.DenyAll(resource, m.AclRole(), acl.Permissions{acl.AccessAll, acl.AccessLibrary})
|
||||
}
|
||||
|
||||
// IsUnknown checks if the user is unknown.
|
||||
|
||||
@@ -57,19 +57,19 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults
|
||||
}
|
||||
|
||||
// Check user permissions.
|
||||
if acl.Resources.DenyAll(aclResource, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary, acl.AccessShared, acl.AccessOwn}) {
|
||||
if acl.Rules.DenyAll(aclResource, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary, acl.AccessShared, acl.AccessOwn}) {
|
||||
return AlbumResults{}, ErrForbidden
|
||||
}
|
||||
|
||||
// Limit results by UID, owner and path.
|
||||
if sess.IsVisitor() || sess.NotRegistered() {
|
||||
s = s.Where("albums.album_uid IN (?) OR albums.published_at > ?", sess.SharedUIDs(), entity.TimeStamp())
|
||||
} else if acl.Resources.DenyAll(aclResource, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) {
|
||||
} else if acl.Rules.DenyAll(aclResource, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) {
|
||||
s = s.Where("albums.album_uid IN (?) OR albums.created_by = ? OR albums.published_at > ?", sess.SharedUIDs(), user.UserUID, entity.TimeStamp())
|
||||
}
|
||||
|
||||
// Exclude private content?
|
||||
if acl.Resources.Deny(acl.ResourcePhotos, aclRole, acl.AccessPrivate) || acl.Resources.Deny(aclResource, aclRole, acl.AccessPrivate) {
|
||||
if acl.Rules.Deny(acl.ResourcePhotos, aclRole, acl.AccessPrivate) || acl.Rules.Deny(aclResource, aclRole, acl.AccessPrivate) {
|
||||
f.Public = true
|
||||
f.Private = false
|
||||
}
|
||||
|
||||
@@ -129,31 +129,31 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string)
|
||||
aclRole := user.AclRole()
|
||||
|
||||
// Exclude private content.
|
||||
if acl.Resources.Deny(acl.ResourcePhotos, aclRole, acl.AccessPrivate) {
|
||||
if acl.Rules.Deny(acl.ResourcePhotos, aclRole, acl.AccessPrivate) {
|
||||
f.Public = true
|
||||
f.Private = false
|
||||
}
|
||||
|
||||
// Exclude archived content.
|
||||
if acl.Resources.Deny(acl.ResourcePhotos, aclRole, acl.ActionDelete) {
|
||||
if acl.Rules.Deny(acl.ResourcePhotos, aclRole, acl.ActionDelete) {
|
||||
f.Archived = false
|
||||
f.Review = false
|
||||
}
|
||||
|
||||
// Exclude hidden files.
|
||||
if acl.Resources.Deny(acl.ResourceFiles, aclRole, acl.AccessAll) {
|
||||
if acl.Rules.Deny(acl.ResourceFiles, aclRole, acl.AccessAll) {
|
||||
f.Hidden = false
|
||||
}
|
||||
|
||||
// Visitors and other restricted users can only access shared content.
|
||||
if f.Scope != "" && !sess.HasShare(f.Scope) && (sess.User().HasSharedAccessOnly(acl.ResourcePhotos) || sess.NotRegistered()) ||
|
||||
f.Scope == "" && acl.Resources.Deny(acl.ResourcePhotos, aclRole, acl.ActionSearch) {
|
||||
f.Scope == "" && acl.Rules.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
|
||||
}
|
||||
|
||||
// Limit results for external users.
|
||||
if f.Scope == "" && acl.Resources.DenyAll(acl.ResourcePhotos, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) {
|
||||
if f.Scope == "" && acl.Rules.DenyAll(acl.ResourcePhotos, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) {
|
||||
sharedAlbums := "photos.photo_uid IN (SELECT photo_uid FROM photos_albums WHERE hidden = 0 AND missing = 0 AND album_uid IN (?)) OR "
|
||||
|
||||
if sess.IsVisitor() || sess.NotRegistered() {
|
||||
|
||||
@@ -117,26 +117,26 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes
|
||||
aclRole := user.AclRole()
|
||||
|
||||
// Exclude private content.
|
||||
if acl.Resources.Deny(acl.ResourcePlaces, aclRole, acl.AccessPrivate) {
|
||||
if acl.Rules.Deny(acl.ResourcePlaces, aclRole, acl.AccessPrivate) {
|
||||
f.Public = true
|
||||
f.Private = false
|
||||
}
|
||||
|
||||
// Exclude archived content.
|
||||
if acl.Resources.Deny(acl.ResourcePlaces, aclRole, acl.ActionDelete) {
|
||||
if acl.Rules.Deny(acl.ResourcePlaces, aclRole, acl.ActionDelete) {
|
||||
f.Archived = false
|
||||
f.Review = false
|
||||
}
|
||||
|
||||
// Visitors and other restricted users can only access shared content.
|
||||
if f.Scope != "" && !sess.HasShare(f.Scope) && (sess.User().HasSharedAccessOnly(acl.ResourcePlaces) || sess.NotRegistered()) ||
|
||||
f.Scope == "" && acl.Resources.Deny(acl.ResourcePlaces, aclRole, acl.ActionSearch) {
|
||||
f.Scope == "" && acl.Rules.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
|
||||
}
|
||||
|
||||
// Limit results for external users.
|
||||
if f.Scope == "" && acl.Resources.DenyAll(acl.ResourcePlaces, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) {
|
||||
if f.Scope == "" && acl.Rules.DenyAll(acl.ResourcePlaces, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) {
|
||||
sharedAlbums := "photos.photo_uid IN (SELECT photo_uid FROM photos_albums WHERE hidden = 0 AND missing = 0 AND album_uid IN (?)) OR "
|
||||
|
||||
if sess.IsVisitor() || sess.NotRegistered() {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// Sessions finds user sessions.
|
||||
@@ -13,20 +12,24 @@ func Sessions(f form.SearchSessions) (result entity.Sessions, err error) {
|
||||
result = entity.Sessions{}
|
||||
stmt := Db()
|
||||
|
||||
userUid := strings.TrimSpace(f.UID)
|
||||
search := strings.TrimSpace(f.Query)
|
||||
uid := strings.TrimSpace(f.UID)
|
||||
|
||||
sortOrder := f.Order
|
||||
limit := f.Count
|
||||
offset := f.Offset
|
||||
|
||||
if search == "all" {
|
||||
// Don't filter.
|
||||
} else if rnd.IsUID(uid, entity.UserUID) {
|
||||
stmt = stmt.Where("user_uid = ?", search)
|
||||
} else if search != "" {
|
||||
// Filter by user UID?
|
||||
if userUid != "" {
|
||||
stmt = stmt.Where("user_uid = ?", userUid)
|
||||
}
|
||||
|
||||
// Filter by user name and/or auth provider name?
|
||||
if search != "" && search != "all" {
|
||||
stmt = stmt.Where("user_name LIKE ? OR auth_provider LIKE ?", search+"%", search+"%")
|
||||
}
|
||||
|
||||
// Sort results?
|
||||
if sortOrder == "" {
|
||||
sortOrder = "last_active DESC, user_name"
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||
api.UploadUserFiles(APIv1)
|
||||
api.ProcessUserUpload(APIv1)
|
||||
api.UploadUserAvatar(APIv1)
|
||||
api.FindUserSessions(APIv1)
|
||||
api.CreateUserPasscode(APIv1)
|
||||
api.ConfirmUserPasscode(APIv1)
|
||||
api.ActivateUserPasscode(APIv1)
|
||||
|
||||
@@ -114,7 +114,7 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
||||
event.AuditErr([]string{clientIp, "session %s", "access webdav without user account", "denied"}, sess.RefID)
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
} else if sess.IsClient() && !sess.HasScope(acl.ResourceWebDAV.String()) {
|
||||
} else if sess.IsClient() && sess.ScopeExcludes(acl.ResourceWebDAV, nil) {
|
||||
// Log error if the client is allowed to access webdav based on its scope.
|
||||
message := "denied"
|
||||
event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.ClientInfo()), sess.RefID, clean.LogQuote(user.Username()))
|
||||
|
||||
@@ -170,7 +170,7 @@ func TestWebDAVAuthSession(t *testing.T) {
|
||||
assert.True(t, sess.HasUser())
|
||||
assert.Equal(t, user.UserUID, sess.UserUID)
|
||||
assert.Equal(t, entity.UserFixtures.Get("alice").UserUID, sess.UserUID)
|
||||
assert.True(t, sess.HasScope(acl.ResourceWebDAV.String()))
|
||||
assert.True(t, sess.ScopeAllows(acl.ResourceWebDAV, acl.Permissions{acl.ActionView}))
|
||||
assert.False(t, cached)
|
||||
|
||||
assert.Equal(t, s.ID, sid)
|
||||
@@ -222,7 +222,7 @@ func TestWebDAVAuthSession(t *testing.T) {
|
||||
assert.Equal(t, entity.UserFixtures.Get("alice").UserUID, user.UserUID)
|
||||
assert.Equal(t, entity.UserFixtures.Get("alice").UserUID, sess.UserUID)
|
||||
assert.True(t, user.CanUseWebDAV())
|
||||
assert.False(t, sess.HasScope(acl.ResourceWebDAV.String()))
|
||||
assert.False(t, sess.ScopeAllows(acl.ResourceWebDAV, acl.Permissions{acl.ActionView}))
|
||||
|
||||
// WebDAVAuthSession should not set a status code or any headers.
|
||||
assert.Equal(t, http.StatusOK, c.Writer.Status())
|
||||
|
||||
@@ -45,7 +45,7 @@ func NewOAuthAuthorizationServer(conf *config.Config) *OAuthAuthorizationServer
|
||||
Issuer: conf.SiteUrl(),
|
||||
AuthorizationEndpoint: "",
|
||||
TokenEndpoint: fmt.Sprintf("%sapi/v1/oauth/token", conf.SiteUrl()),
|
||||
ScopesSupported: acl.Resources.Resources(),
|
||||
ScopesSupported: acl.Rules.Resources(),
|
||||
ResponseTypesSupported: OAuthResponseTypes,
|
||||
GrantTypesSupported: OAuthGrantTypes,
|
||||
TokenEndpointAuthMethodsSupported: OAuthTokenEndpointAuthMethods,
|
||||
|
||||
@@ -49,7 +49,7 @@ func NewOpenIDConfiguration(conf *config.Config) *OpenIDConfiguration {
|
||||
GrantTypesSupported: OAuthGrantTypes,
|
||||
SubjectTypesSupported: []string{},
|
||||
IdTokenSigningAlgValuesSupported: []string{},
|
||||
ScopesSupported: acl.Resources.Resources(),
|
||||
ScopesSupported: acl.Rules.Resources(),
|
||||
TokenEndpointAuthMethodsSupported: OAuthTokenEndpointAuthMethods,
|
||||
ClaimsSupported: []string{},
|
||||
CodeChallengeMethodsSupported: []string{},
|
||||
|
||||
@@ -74,7 +74,9 @@ func (f *KeyValue) Parse(s string) *KeyValue {
|
||||
}
|
||||
|
||||
// Default?
|
||||
if v = Value(v); v == "" {
|
||||
if f.Key == All {
|
||||
return f
|
||||
} else if v = Value(v); v == "" {
|
||||
f.Value = True
|
||||
return f
|
||||
}
|
||||
@@ -95,6 +97,10 @@ func (f *KeyValue) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
if f.Key == All {
|
||||
return All
|
||||
}
|
||||
|
||||
if Bool[strings.ToLower(f.Value)] == True {
|
||||
return f.Key
|
||||
}
|
||||
|
||||
@@ -59,45 +59,72 @@ func (list Attr) String() string {
|
||||
}
|
||||
|
||||
// Sort sorts the attributes by key.
|
||||
func (list Attr) Sort() {
|
||||
func (list Attr) Sort() Attr {
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
if list[i].Key == list[j].Key {
|
||||
return list[i].Value < list[j].Value
|
||||
} else if list[i].Key == All {
|
||||
return false
|
||||
} else if list[j].Key == All {
|
||||
return true
|
||||
} else {
|
||||
return list[i].Key < list[j].Key
|
||||
}
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// Contains tests if the list contains the attribute provided as string.
|
||||
func (list Attr) Contains(s string) bool {
|
||||
if len(list) == 0 || s == "" {
|
||||
attr := list.Find(s)
|
||||
|
||||
if attr.Key == "" || attr.Value == False {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Find returns the matching KeyValue attribute if found.
|
||||
func (list Attr) Find(s string) (a KeyValue) {
|
||||
if len(list) == 0 || s == "" {
|
||||
return a
|
||||
} else if s == All {
|
||||
return true
|
||||
return KeyValue{Key: All, Value: ""}
|
||||
}
|
||||
|
||||
attr := ParseKeyValue(s)
|
||||
|
||||
// Abort if attribute is invalid.
|
||||
// Return nil if key is invalid or all.
|
||||
if attr.Key == "" {
|
||||
return false
|
||||
return a
|
||||
}
|
||||
|
||||
// Find matches.
|
||||
// Find and return first match.
|
||||
if attr.Value == "" || attr.Value == All {
|
||||
for i := range list {
|
||||
if strings.EqualFold(attr.Key, list[i].Key) || list[i].Key == All {
|
||||
return true
|
||||
if strings.EqualFold(attr.Key, list[i].Key) {
|
||||
return *list[i]
|
||||
} else if list[i].Key == All {
|
||||
a = *list[i]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := range list {
|
||||
if strings.EqualFold(attr.Key, list[i].Key) && (attr.Value == list[i].Value || list[i].Value == All) || list[i].Key == All {
|
||||
return true
|
||||
if strings.EqualFold(attr.Key, list[i].Key) {
|
||||
if attr.Value == True && list[i].Value == False {
|
||||
return KeyValue{Key: "", Value: ""}
|
||||
} else if attr.Value == list[i].Value {
|
||||
return *list[i]
|
||||
} else if list[i].Value == All {
|
||||
a = *list[i]
|
||||
}
|
||||
} else if list[i].Key == All && attr.Value != False {
|
||||
a = *list[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
return a
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ func TestParseAttr(t *testing.T) {
|
||||
|
||||
assert.Len(t, attr, 3)
|
||||
assert.Equal(t, Attr{{Key: "foo", Value: "true"}, {Key: "bar", Value: "true"}, {Key: "baz", Value: "true"}}, attr)
|
||||
assert.Equal(t, Attr{{Key: "bar", Value: "true"}, {Key: "baz", Value: "true"}, {Key: "foo", Value: "true"}}, attr.Sort())
|
||||
})
|
||||
t.Run("WhitespaceKeys", func(t *testing.T) {
|
||||
attr := ParseAttr(" foo bar baz ")
|
||||
|
||||
assert.Len(t, attr, 3)
|
||||
assert.Equal(t, Attr{{Key: "foo", Value: "true"}, {Key: "bar", Value: "true"}, {Key: "baz", Value: "true"}}, attr)
|
||||
assert.Equal(t, Attr{{Key: "bar", Value: "true"}, {Key: "baz", Value: "true"}, {Key: "foo", Value: "true"}}, attr.Sort())
|
||||
})
|
||||
t.Run("Values", func(t *testing.T) {
|
||||
attr := ParseAttr("foo:yes bar:disable baz:true biZZ:false BIG CAT:FISH berghain:berlin:germany hello:off")
|
||||
@@ -41,6 +43,18 @@ func TestParseAttr(t *testing.T) {
|
||||
{Key: "hello", Value: "false"},
|
||||
}, attr,
|
||||
)
|
||||
assert.Equal(t,
|
||||
Attr{
|
||||
{Key: "BIG", Value: "true"},
|
||||
{Key: "CAT", Value: "FISH"},
|
||||
{Key: "bar", Value: "false"},
|
||||
{Key: "baz", Value: "true"},
|
||||
{Key: "berghain", Value: "berlin:germany"},
|
||||
{Key: "biZZ", Value: "false"},
|
||||
{Key: "foo", Value: "true"},
|
||||
{Key: "hello", Value: "false"},
|
||||
}, attr.Sort(),
|
||||
)
|
||||
})
|
||||
t.Run("Scopes", func(t *testing.T) {
|
||||
attr := ParseAttr("files files.read:true photos photos.create:false albums:true people.view:true config.view:false")
|
||||
@@ -55,6 +69,15 @@ func TestParseAttr(t *testing.T) {
|
||||
{Key: "people.view", Value: "true"},
|
||||
{Key: "config.view", Value: "false"},
|
||||
}, attr)
|
||||
assert.Equal(t, Attr{
|
||||
{Key: "albums", Value: "true"},
|
||||
{Key: "config.view", Value: "false"},
|
||||
{Key: "files", Value: "true"},
|
||||
{Key: "files.read", Value: "true"},
|
||||
{Key: "people.view", Value: "true"},
|
||||
{Key: "photos", Value: "true"},
|
||||
{Key: "photos.create", Value: "false"},
|
||||
}, attr.Sort())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -63,7 +86,7 @@ func TestParseKeyValue(t *testing.T) {
|
||||
kv := ParseKeyValue("*")
|
||||
|
||||
assert.Equal(t, "*", kv.Key)
|
||||
assert.Equal(t, "true", kv.Value)
|
||||
assert.Equal(t, "", kv.Value)
|
||||
})
|
||||
t.Run("Scope", func(t *testing.T) {
|
||||
kv := ParseKeyValue("files.read:true")
|
||||
@@ -88,11 +111,11 @@ func TestAttr_String(t *testing.T) {
|
||||
assert.Equal(t, s, attr.String())
|
||||
})
|
||||
t.Run("Random", func(t *testing.T) {
|
||||
s := " admin.conversations.removeCustomRetention admin.usergroups:read me:yes FOOt0-2U 6VU #$#%$ cm,Nu"
|
||||
s := " admin.conversations.removeCustomRetention * admin.usergroups:read me:yes FOOt0-2U 6VU #$#%$ cm,Nu"
|
||||
attr := ParseAttr(s)
|
||||
|
||||
assert.Len(t, attr, 6)
|
||||
assert.Equal(t, "6VU FOOt0-2U admin.conversations.removeCustomRetention admin.usergroups:read cmNu me", attr.String())
|
||||
assert.Len(t, attr, 7)
|
||||
assert.Equal(t, "6VU FOOt0-2U admin.conversations.removeCustomRetention admin.usergroups:read cmNu me *", attr.String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -132,3 +155,94 @@ func TestAttr_Contains(t *testing.T) {
|
||||
assert.True(t, attr.Contains("people.view"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAttr_Find(t *testing.T) {
|
||||
t.Run("Any", func(t *testing.T) {
|
||||
s := "*"
|
||||
attr := ParseAttr(s)
|
||||
|
||||
assert.Len(t, attr, 1)
|
||||
result := attr.Find("metrics")
|
||||
|
||||
assert.Equal(t, All, result.Key)
|
||||
assert.Equal(t, "", result.Value)
|
||||
})
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
s := "*"
|
||||
attr := ParseAttr(s)
|
||||
|
||||
assert.Len(t, attr, 1)
|
||||
result := attr.Find("")
|
||||
assert.Equal(t, "", result.Key)
|
||||
assert.Equal(t, "", result.Value)
|
||||
})
|
||||
t.Run("All", func(t *testing.T) {
|
||||
s := "*"
|
||||
attr := ParseAttr(s)
|
||||
|
||||
assert.Len(t, attr, 1)
|
||||
result := attr.Find("*")
|
||||
assert.Equal(t, All, result.Key)
|
||||
assert.Equal(t, "", result.Value)
|
||||
})
|
||||
t.Run("ValueAll", func(t *testing.T) {
|
||||
s := "*"
|
||||
attr := ParseAttr(s)
|
||||
|
||||
assert.Len(t, attr, 1)
|
||||
result := attr.Find("6VU:*")
|
||||
assert.Equal(t, All, result.Key)
|
||||
assert.Equal(t, "", result.Value)
|
||||
})
|
||||
t.Run("Scopes", func(t *testing.T) {
|
||||
attr := ParseAttr("files files.read:true photos photos.create:false albums:true people.view:true config.view:false")
|
||||
|
||||
assert.Len(t, attr, 7)
|
||||
result := attr.Find("people.view")
|
||||
assert.Equal(t, "people.view", result.Key)
|
||||
assert.Equal(t, True, result.Value)
|
||||
})
|
||||
t.Run("ReadAll", func(t *testing.T) {
|
||||
s := "read *"
|
||||
attr := ParseAttr(s)
|
||||
|
||||
assert.Len(t, attr, 2)
|
||||
result := attr.Find("read")
|
||||
assert.Equal(t, "read", result.Key)
|
||||
assert.Equal(t, True, result.Value)
|
||||
})
|
||||
t.Run("ReadFalse", func(t *testing.T) {
|
||||
s := "read:false *"
|
||||
attr := ParseAttr(s)
|
||||
|
||||
assert.Len(t, attr, 2)
|
||||
result := attr.Find("read:*")
|
||||
assert.Equal(t, "read", result.Key)
|
||||
assert.Equal(t, False, result.Value)
|
||||
result = attr.Find("read:false")
|
||||
assert.Equal(t, "read", result.Key)
|
||||
assert.Equal(t, False, result.Value)
|
||||
})
|
||||
t.Run("ReadOther", func(t *testing.T) {
|
||||
s := "read:other *"
|
||||
attr := ParseAttr(s)
|
||||
|
||||
assert.Len(t, attr, 2)
|
||||
|
||||
result := attr.Find("read")
|
||||
assert.Equal(t, All, result.Key)
|
||||
assert.Equal(t, "", result.Value)
|
||||
|
||||
result = attr.Find("read:other")
|
||||
assert.Equal(t, "read", result.Key)
|
||||
assert.Equal(t, "other", result.Value)
|
||||
|
||||
result = attr.Find("read:true")
|
||||
assert.Equal(t, All, result.Key)
|
||||
assert.Equal(t, "", result.Value)
|
||||
|
||||
result = attr.Find("read:false")
|
||||
assert.Equal(t, "", result.Key)
|
||||
assert.Equal(t, "", result.Value)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user