Account: Add GET /api/v1/users/:uid/sessions endpoint #808 #4114

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2024-03-28 13:23:55 +01:00
parent d76f0e361e
commit 1912cd54ca
50 changed files with 851 additions and 268 deletions

View File

@@ -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)
}

View File

@@ -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
View 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"
)

View File

@@ -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
}

View File

@@ -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}))
})
}

View File

@@ -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])
})

View File

@@ -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, ", ")
}

View 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, ", ")
}

View File

@@ -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"
)

View File

@@ -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,

View File

@@ -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

View File

@@ -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
View 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,
}
)

View 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}))
})
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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.

View File

@@ -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"))

View 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)
})
}

View 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())
})
}

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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,

View File

@@ -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)
})

View File

@@ -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()

View File

@@ -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.

View File

@@ -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))

View File

@@ -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.

View File

@@ -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{

View File

@@ -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.

View File

@@ -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
}

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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"
}

View File

@@ -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)

View File

@@ -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()))

View File

@@ -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())

View File

@@ -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,

View File

@@ -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{},

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
})
}