This also adds the ability to change the client role if needed and improves the usage information and output of the CLI commands. Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
392bb1d5cf
commit
7e7ba69982
44 changed files with 530 additions and 238 deletions
|
@ -65,10 +65,6 @@ func (a *AuthRequest) GetClientID() string {
|
|||
return a.ClientID
|
||||
}
|
||||
|
||||
//func (a *AuthRequest) GetCode() string {
|
||||
// return "GetCode"
|
||||
//}
|
||||
//
|
||||
func (a *AuthRequest) GetCodeChallenge() *oidc.CodeChallenge {
|
||||
fmt.Println("GetCodeChallenge: ", a.CodeChallenge.Challenge, a.CodeChallenge.Method)
|
||||
return a.CodeChallenge
|
||||
|
|
|
@ -16,8 +16,8 @@ func (r Role) String() string {
|
|||
|
||||
// Pretty returns the type in an easy-to-read format.
|
||||
func (r Role) Pretty() string {
|
||||
if r == RoleUnknown {
|
||||
return "Unknown"
|
||||
if r == RoleNone {
|
||||
return "None"
|
||||
}
|
||||
|
||||
return txt.UpperFirst(string(r))
|
||||
|
@ -40,7 +40,7 @@ func (r Role) NotEqual(s string) bool {
|
|||
|
||||
// Valid checks if the role is valid.
|
||||
func (r Role) Valid(s string) bool {
|
||||
return ValidRoles[s] != ""
|
||||
return UserRoles[s] != ""
|
||||
}
|
||||
|
||||
// Invalid checks if the role is invalid.
|
||||
|
|
|
@ -22,8 +22,8 @@ func TestRole_Pretty(t *testing.T) {
|
|||
t.Run("Admin", func(t *testing.T) {
|
||||
assert.Equal(t, "Admin", RoleAdmin.Pretty())
|
||||
})
|
||||
t.Run("Unknown", func(t *testing.T) {
|
||||
assert.Equal(t, "Unknown", RoleUnknown.Pretty())
|
||||
t.Run("None", func(t *testing.T) {
|
||||
assert.Equal(t, "None", RoleNone.Pretty())
|
||||
})
|
||||
t.Run("Visitor", func(t *testing.T) {
|
||||
assert.Equal(t, "Visitor", RoleVisitor.Pretty())
|
||||
|
|
|
@ -6,17 +6,24 @@ const (
|
|||
RoleAdmin Role = "admin"
|
||||
RoleVisitor Role = "visitor"
|
||||
RoleClient Role = "client"
|
||||
RoleUnknown Role = ""
|
||||
RoleNone Role = ""
|
||||
)
|
||||
|
||||
// RoleStrings represents user role names mapped to roles.
|
||||
type RoleStrings = map[string]Role
|
||||
|
||||
// ValidRoles specifies the valid user roles.
|
||||
var ValidRoles = RoleStrings{
|
||||
// UserRoles maps valid user account roles.
|
||||
var UserRoles = RoleStrings{
|
||||
string(RoleAdmin): RoleAdmin,
|
||||
string(RoleVisitor): RoleVisitor,
|
||||
string(RoleUnknown): RoleUnknown,
|
||||
string(RoleNone): RoleNone,
|
||||
}
|
||||
|
||||
// ClientRoles maps valid API client roles.
|
||||
var ClientRoles = RoleStrings{
|
||||
string(RoleAdmin): RoleAdmin,
|
||||
string(RoleClient): RoleClient,
|
||||
string(RoleNone): RoleNone,
|
||||
}
|
||||
|
||||
// Roles grants permissions to roles.
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
)
|
||||
|
||||
|
@ -42,31 +43,31 @@ func AuthAny(c *gin.Context, resource acl.Resource, grants acl.Permissions) (s *
|
|||
if s.IsClient() {
|
||||
// Check ACL resource name against the permitted scope.
|
||||
if !s.HasScope(resource.String()) {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "access %s", "denied"}, s.AuthID, s.RefID, string(resource))
|
||||
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, acl.RoleClient, grants) {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "%s %s", "denied"}, s.AuthID, s.RefID, grants.String(), string(resource))
|
||||
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))
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
// Additionally check the user authorization if the client belongs to a user account.
|
||||
if s.NoUser() {
|
||||
// Allow access based on the ACL defaults for client applications.
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s", "granted"}, s.AuthID, s.RefID, grants.String(), string(resource))
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s", "granted"}, clean.Log(s.ClientInfo()), s.RefID, grants.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"}, s.AuthID, s.RefID, grants.String(), string(resource), u.String())
|
||||
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())
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
// Allow access based on the user role.
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "%s %s as %s", "granted"}, s.AuthID, 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, grants.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"}, s.AuthID, 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, grants.String(), string(resource))
|
||||
return entity.SessionStatusForbidden()
|
||||
}
|
||||
|
||||
|
|
|
@ -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.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
if f.All && !acl.Resources.AllowAll(acl.ResourcePhotos, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
AbortForbidden(c)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ func SaveSettings(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
if acl.Resources.DenyAll(acl.ResourceSettings, s.User().AclRole(), acl.Permissions{acl.ActionUpdate, acl.ActionManage}) {
|
||||
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())))
|
||||
return
|
||||
} else if err := user.Settings().Apply(settings).Save(); err != nil {
|
||||
|
|
|
@ -67,9 +67,9 @@ func StartImport(router *gin.RouterGroup) {
|
|||
// To avoid conflicts, uploads are imported from "import_path/upload/session_ref/timestamp".
|
||||
if token := path.Base(srcFolder); token != "" && path.Dir(srcFolder) == UploadPath {
|
||||
srcFolder = path.Join(UploadPath, s.RefID+token)
|
||||
event.AuditInfo([]string{ClientIP(c), "session %s", "import uploads from %s as %s", "granted"}, s.RefID, clean.Log(srcFolder), s.User().AclRole().String())
|
||||
} else if acl.Resources.Deny(acl.ResourceFiles, s.User().AclRole(), acl.ActionManage) {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s", "import files from %s as %s", "denied"}, s.RefID, clean.Log(srcFolder), s.User().AclRole().String())
|
||||
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) {
|
||||
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.User().AclRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
||||
acl.Resources.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.User().AclRole(), acl.ActionManage) {
|
||||
acl.Resources.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.User().AclRole(), acl.ActionManage) {
|
||||
acl.Resources.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.User().AclRole(), acl.ActionReact) {
|
||||
if get.Config().Experimental() && acl.Resources.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionReact) {
|
||||
logWarn("react", m.React(s.User(), react.Find("love")))
|
||||
}
|
||||
|
||||
if acl.Resources.Allow(acl.ResourcePhotos, s.User().AclRole(), acl.ActionUpdate) {
|
||||
if acl.Resources.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.User().AclRole(), acl.ActionReact) {
|
||||
if get.Config().Experimental() && acl.Resources.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionReact) {
|
||||
logWarn("react", m.UnReact(s.User()))
|
||||
}
|
||||
|
||||
if acl.Resources.Allow(acl.ResourcePhotos, s.User().AclRole(), acl.ActionUpdate) {
|
||||
if acl.Resources.Allow(acl.ResourcePhotos, s.UserRole(), acl.ActionUpdate) {
|
||||
err = m.SetFavorite(false)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -49,27 +49,27 @@ 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.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
event.AuditErr([]string{clientIp, "session %s", "delete %s as %s", "denied"}, s.RefID, acl.ResourceSessions.String(), s.User().AclRole())
|
||||
if !acl.Resources.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
|
||||
}
|
||||
|
||||
event.AuditInfo([]string{clientIp, "session %s", "delete %s as %s", "granted"}, s.RefID, acl.ResourceSessions.String(), s.User().AclRole())
|
||||
event.AuditInfo([]string{clientIp, "session %s", "delete %s as %s", "granted"}, s.RefID, acl.ResourceSessions.String(), s.UserRole())
|
||||
|
||||
if s = entity.FindSessionByRefID(id); s == nil {
|
||||
Abort(c, http.StatusNotFound, i18n.ErrNotFound)
|
||||
return
|
||||
}
|
||||
} else if id != "" && s.ID != id {
|
||||
event.AuditWarn([]string{clientIp, "session %s", "delete %s as %s", "ids do not match"}, s.RefID, acl.ResourceSessions.String(), s.User().AclRole())
|
||||
event.AuditWarn([]string{clientIp, "session %s", "delete %s as %s", "ids do not match"}, s.RefID, acl.ResourceSessions.String(), s.UserRole())
|
||||
Abort(c, http.StatusForbidden, i18n.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete session cache and database record.
|
||||
if err := s.Delete(); err != nil {
|
||||
event.AuditErr([]string{clientIp, "session %s", "delete session as %s", "%s"}, s.RefID, s.User().AclRole(), err)
|
||||
event.AuditErr([]string{clientIp, "session %s", "delete session as %s", "%s"}, s.RefID, s.UserRole(), err)
|
||||
} else {
|
||||
event.AuditDebug([]string{clientIp, "session %s", "deleted"}, s.RefID)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/dustin/go-humanize/english"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
|
@ -73,7 +72,7 @@ func CreateOAuthToken(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Find the client that has the ID specified in the authentication request.
|
||||
client := entity.FindClient(f.ClientID)
|
||||
client := entity.FindClientByUID(f.ClientID)
|
||||
|
||||
// Abort if the client ID or secret are invalid.
|
||||
if client == nil {
|
||||
|
@ -181,28 +180,28 @@ func RevokeOAuthToken(router *gin.RouterGroup) {
|
|||
sess, err := entity.FindSession(rnd.SessionID(f.AuthToken))
|
||||
|
||||
if err != nil {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String(), err.Error())
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String(), err.Error())
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, i18n.NewResponse(http.StatusUnauthorized, i18n.ErrUnauthorized))
|
||||
return
|
||||
} else if sess == nil {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String())
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, i18n.NewResponse(http.StatusUnauthorized, i18n.ErrUnauthorized))
|
||||
return
|
||||
} else if sess.Abort(c) {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String())
|
||||
return
|
||||
} else if !sess.IsClient() {
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "denied"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String())
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, i18n.NewResponse(http.StatusForbidden, i18n.ErrForbidden))
|
||||
return
|
||||
} else {
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "delete session as %s", "granted"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String())
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "delete session as %s", "granted"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String())
|
||||
}
|
||||
|
||||
// Delete session cache and database record.
|
||||
if err = sess.Delete(); err != nil {
|
||||
// Log error.
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.AuthID), clean.Log(sess.RefID), acl.RoleClient.String(), err)
|
||||
event.AuditErr([]string{clientIp, "client %s", "session %s", "delete session as %s", "%s"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID), sess.ClientRole().String(), err)
|
||||
|
||||
// Return JSON error.
|
||||
c.AbortWithStatusJSON(http.StatusNotFound, i18n.NewResponse(http.StatusNotFound, i18n.ErrNotFound))
|
||||
|
@ -210,7 +209,7 @@ func RevokeOAuthToken(router *gin.RouterGroup) {
|
|||
}
|
||||
|
||||
// Log event.
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "deleted"}, clean.Log(sess.AuthID), clean.Log(sess.RefID))
|
||||
event.AuditInfo([]string{clientIp, "client %s", "session %s", "deleted"}, clean.Log(sess.ClientInfo()), clean.Log(sess.RefID))
|
||||
|
||||
// Return JSON response for confirmation.
|
||||
c.JSON(http.StatusOK, DeleteSessionResponse(sess.ID))
|
||||
|
|
|
@ -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.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
isAdmin := acl.Resources.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 session user is has user management privileges.
|
||||
isAdmin := acl.Resources.AllowAll(acl.ResourceUsers, s.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
isAdmin := acl.Resources.AllowAll(acl.ResourceUsers, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
isSuperAdmin := isAdmin && s.User().IsSuperAdmin()
|
||||
uid := clean.UID(c.Param("uid"))
|
||||
|
||||
|
|
|
@ -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.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
isAdmin := acl.Resources.AllowAll(acl.ResourceUsers, s.UserRole(), acl.Permissions{acl.AccessAll, acl.ActionManage})
|
||||
privilegeLevelChange := isAdmin && m.PrivilegeLevelChange(f)
|
||||
|
||||
// Prevent super admins from locking themselves out.
|
||||
|
|
|
@ -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.User().AclRole(), acl.Permissions{acl.ActionCreate, acl.ActionUpload}) {
|
||||
acl.Resources.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
|
||||
}
|
||||
|
|
|
@ -17,15 +17,15 @@ import (
|
|||
var AuthAddFlags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "name, n",
|
||||
Usage: "arbitrary name to help identify the access `TOKEN`",
|
||||
Usage: "access `TOKEN` name to help identify the client application",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "scope, s",
|
||||
Usage: "authorization `SCOPE` for the access token e.g. \"metrics\" or \"photos albums\" (\"*\" to allow all scopes)",
|
||||
Usage: "authorization `SCOPES` e.g. \"metrics\" or \"photos albums\" (\"*\" to allow all)",
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "expires, e",
|
||||
Usage: "access token lifetime in `SECONDS`, after which it expires and a new token must be created (-1 to disable)",
|
||||
Usage: "authentication `LIFETIME` in seconds, after which the access token expires (-1 to disable the limit)",
|
||||
Value: entity.UnixYear,
|
||||
},
|
||||
}
|
||||
|
@ -53,10 +53,10 @@ func authAddAction(ctx *cli.Context) error {
|
|||
return fmt.Errorf("user %s not found", clean.LogQuote(userName))
|
||||
}
|
||||
|
||||
// Get token name from command flag or ask for it.
|
||||
tokenName := ctx.String("name")
|
||||
// Get client name from command flag or ask for it.
|
||||
clientName := ctx.String("name")
|
||||
|
||||
if tokenName == "" {
|
||||
if clientName == "" {
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Token Name",
|
||||
Default: rnd.Name(),
|
||||
|
@ -68,7 +68,7 @@ func authAddAction(ctx *cli.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
tokenName = clean.Name(res)
|
||||
clientName = clean.Name(res)
|
||||
}
|
||||
|
||||
// Get auth scope from command flag or ask for it.
|
||||
|
@ -89,8 +89,8 @@ func authAddAction(ctx *cli.Context) error {
|
|||
authScope = clean.Scope(res)
|
||||
}
|
||||
|
||||
// Create client session.
|
||||
sess, err := entity.CreateClientAccessToken(tokenName, ctx.Int64("expires"), authScope, user)
|
||||
// Create session with client access token.
|
||||
sess, err := entity.CreateClientAccessToken(clientName, ctx.Int64("expires"), authScope, user)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create access token: %s", err)
|
||||
|
|
|
@ -25,7 +25,7 @@ func authListAction(ctx *cli.Context) error {
|
|||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
var rows [][]string
|
||||
|
||||
cols := []string{"Session ID", "User", "Authentication", "Scope", "Identifier", "Client IP", "Last Active", "Created At", "Expires At"}
|
||||
cols := []string{"Session ID", "Session User", "Session Client", "Authentication Method", "Scope", "Login IP", "Current IP", "Last Active", "Created At", "Expires At"}
|
||||
|
||||
if ctx.Bool("tokens") {
|
||||
cols = append(cols, "Preview Token", "Download Token")
|
||||
|
@ -49,18 +49,13 @@ func authListAction(ctx *cli.Context) error {
|
|||
|
||||
// Display report.
|
||||
for i, res := range results {
|
||||
user := res.Username()
|
||||
|
||||
if user == "" {
|
||||
user = res.User().UserRole
|
||||
}
|
||||
|
||||
rows[i] = []string{
|
||||
res.RefID,
|
||||
user,
|
||||
res.UserInfo(),
|
||||
res.ClientInfo(),
|
||||
res.AuthInfo(),
|
||||
res.AuthScope,
|
||||
res.AuthID,
|
||||
res.LoginIP,
|
||||
res.ClientIP,
|
||||
report.UnixTime(res.LastActive),
|
||||
report.DateTime(&res.CreatedAt),
|
||||
|
|
|
@ -41,7 +41,7 @@ func TestAuthListCommand(t *testing.T) {
|
|||
// Check command output for plausibility.
|
||||
// t.Logf(output)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, output, "| Session ID |")
|
||||
assert.Contains(t, output, "Session ID")
|
||||
assert.Contains(t, output, "alice")
|
||||
assert.NotContains(t, output, "bob")
|
||||
assert.NotContains(t, output, "visitor")
|
||||
|
@ -60,7 +60,7 @@ func TestAuthListCommand(t *testing.T) {
|
|||
// Check command output for plausibility.
|
||||
t.Logf(output)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, output, "Session ID;User;Authentication")
|
||||
assert.Contains(t, output, "Session ID;")
|
||||
assert.Contains(t, output, "alice")
|
||||
assert.NotContains(t, output, "bob")
|
||||
assert.NotContains(t, output, "visitor")
|
||||
|
|
|
@ -3,19 +3,22 @@ package commands
|
|||
import (
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
)
|
||||
|
||||
// Usage hints for the client management subcommands.
|
||||
const (
|
||||
ClientNameUsage = "arbitrary name to help identify the `CLIENT` application"
|
||||
ClientAuthScope = "authorization `SCOPE` of the client e.g. \"metrics\" or \"photos albums\" (\"*\" to allow all scopes)"
|
||||
ClientAuthMethod = "supported authentication `METHOD` for the client application"
|
||||
ClientAuthExpires = "access token lifetime in `SECONDS`, after which a new token must be created by the client (-1 to disable)"
|
||||
ClientAuthTokens = "maximum `NUMBER` of access tokens the client can create (-1 to disable)"
|
||||
ClientNameUsage = "`CLIENT` name to help identify the application"
|
||||
ClientRoleUsage = "client authorization `ROLE`"
|
||||
ClientAuthScope = "client authorization `SCOPES` e.g. \"metrics\" or \"photos albums\" (\"*\" to allow all)"
|
||||
ClientAuthMethod = "client authentication `METHOD`"
|
||||
ClientAuthExpires = "authentication `LIFETIME` in seconds, after which a new access token must be requested (-1 to disable the limit)"
|
||||
ClientAuthTokens = "maximum 'NUMBER' of access tokens that the client can request (-1 to disable the limit)"
|
||||
ClientRegenerateSecret = "generate a new client secret and display it"
|
||||
ClientDisable = "deactivate authentication with this client"
|
||||
ClientEnable = "re-enable client authentication"
|
||||
ClientEnable = "enable client authentication if disabled"
|
||||
ClientDisable = "disable client authentication"
|
||||
)
|
||||
|
||||
// ClientsCommands configures the client application subcommands.
|
||||
|
@ -39,6 +42,11 @@ var ClientAddFlags = []cli.Flag{
|
|||
Name: "name, n",
|
||||
Usage: ClientNameUsage,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "role, r",
|
||||
Usage: ClientRoleUsage,
|
||||
Value: acl.RoleClient.String(),
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "scope, s",
|
||||
Usage: ClientAuthScope,
|
||||
|
@ -52,10 +60,12 @@ var ClientAddFlags = []cli.Flag{
|
|||
cli.Int64Flag{
|
||||
Name: "expires, e",
|
||||
Usage: ClientAuthExpires,
|
||||
Value: entity.UnixDay,
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "tokens, t",
|
||||
Usage: ClientAuthTokens,
|
||||
Value: 10,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -65,6 +75,11 @@ var ClientModFlags = []cli.Flag{
|
|||
Name: "name, n",
|
||||
Usage: ClientNameUsage,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "role, r",
|
||||
Usage: ClientRoleUsage,
|
||||
Value: acl.RoleClient.String(),
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "scope, s",
|
||||
Usage: ClientAuthScope,
|
||||
|
@ -87,12 +102,12 @@ var ClientModFlags = []cli.Flag{
|
|||
Name: "regenerate-secret, r",
|
||||
Usage: ClientRegenerateSecret,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "disable",
|
||||
Usage: ClientDisable,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "enable",
|
||||
Usage: ClientEnable,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "disable",
|
||||
Usage: ClientDisable,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -87,18 +87,9 @@ func clientsAddAction(ctx *cli.Context) error {
|
|||
log.Infof("successfully registered new client %s", clean.LogQuote(client.ClientName))
|
||||
|
||||
// Display client details.
|
||||
cols := []string{"Client ID", "Client Name", "Authentication", "Scope", "User", "Enabled", "Access Token Expires", "Created At"}
|
||||
cols := []string{"Client ID", "Client Name", "Client User", "Authentication Method", "Role", "Scope", "Enabled", "Authentication Expires", "Created At"}
|
||||
rows := make([][]string, 1)
|
||||
|
||||
var userName string
|
||||
if client.UserUID == "" {
|
||||
userName = report.NotAssigned
|
||||
} else if client.UserName != "" {
|
||||
userName = client.UserName
|
||||
} else {
|
||||
userName = client.UserUID
|
||||
}
|
||||
|
||||
var authExpires string
|
||||
if client.AuthExpires > 0 {
|
||||
authExpires = client.Expires().String()
|
||||
|
@ -107,15 +98,16 @@ func clientsAddAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
if client.AuthTokens > 0 {
|
||||
authExpires = fmt.Sprintf("%s, max %d tokens", authExpires, client.AuthTokens)
|
||||
authExpires = fmt.Sprintf("%s; up to %d tokens", authExpires, client.AuthTokens)
|
||||
}
|
||||
|
||||
rows[0] = []string{
|
||||
client.UID(),
|
||||
client.ClientName,
|
||||
client.AuthMethod,
|
||||
client.AuthScope,
|
||||
userName,
|
||||
client.Name(),
|
||||
client.UserInfo(),
|
||||
client.Method().String(),
|
||||
client.AclRole().String(),
|
||||
client.Scope(),
|
||||
report.Bool(client.AuthEnabled, report.Yes, report.No),
|
||||
authExpires,
|
||||
client.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
|
|
|
@ -23,7 +23,7 @@ var ClientsListCommand = cli.Command{
|
|||
// clientsListAction lists registered client applications
|
||||
func clientsListAction(ctx *cli.Context) error {
|
||||
return CallWithDependencies(ctx, func(conf *config.Config) error {
|
||||
cols := []string{"Client ID", "Client Name", "Authentication", "Scope", "User", "Enabled", "Access Token Expires", "Created At"}
|
||||
cols := []string{"Client ID", "Client Name", "Client User", "Authentication Method", "Role", "Scope", "Enabled", "Authentication Expires", "Created At"}
|
||||
|
||||
// Fetch clients from database.
|
||||
clients, err := query.Clients(ctx.Int("n"), 0, "", ctx.Args().First())
|
||||
|
@ -44,15 +44,6 @@ func clientsListAction(ctx *cli.Context) error {
|
|||
|
||||
// Display report.
|
||||
for i, client := range clients {
|
||||
var userName string
|
||||
if client.UserUID == "" {
|
||||
userName = report.NotAssigned
|
||||
} else if client.UserName != "" {
|
||||
userName = client.UserName
|
||||
} else {
|
||||
userName = client.UserUID
|
||||
}
|
||||
|
||||
var authExpires string
|
||||
if client.AuthExpires > 0 {
|
||||
authExpires = client.Expires().String()
|
||||
|
@ -61,15 +52,16 @@ func clientsListAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
if client.AuthTokens > 0 {
|
||||
authExpires = fmt.Sprintf("%s, max %d tokens", authExpires, client.AuthTokens)
|
||||
authExpires = fmt.Sprintf("%s; up to %d tokens", authExpires, client.AuthTokens)
|
||||
}
|
||||
|
||||
rows[i] = []string{
|
||||
client.UID(),
|
||||
client.ClientName,
|
||||
client.AuthMethod,
|
||||
client.AuthScope,
|
||||
userName,
|
||||
client.Name(),
|
||||
client.UserInfo(),
|
||||
client.Method().String(),
|
||||
client.AclRole().String(),
|
||||
client.Scope(),
|
||||
report.Bool(client.AuthEnabled, report.Yes, report.No),
|
||||
authExpires,
|
||||
client.CreatedAt.Format("2006-01-02 15:04:05"),
|
||||
|
|
|
@ -37,7 +37,7 @@ func clientsModAction(ctx *cli.Context) error {
|
|||
// Find client record.
|
||||
var client *entity.Client
|
||||
|
||||
client = entity.FindClient(id)
|
||||
client = entity.FindClientByUID(id)
|
||||
|
||||
if client == nil {
|
||||
return fmt.Errorf("client %s not found", clean.LogQuote(id))
|
||||
|
|
|
@ -41,7 +41,7 @@ func clientsRemoveAction(ctx *cli.Context) error {
|
|||
// Find client record.
|
||||
var m *entity.Client
|
||||
|
||||
m = entity.FindClient(id)
|
||||
m = entity.FindClientByUID(id)
|
||||
|
||||
if m == nil {
|
||||
return fmt.Errorf("client %s not found", clean.LogQuote(id))
|
||||
|
|
|
@ -33,7 +33,7 @@ func clientsShowAction(ctx *cli.Context) error {
|
|||
// Find client record.
|
||||
var m *entity.Client
|
||||
|
||||
m = entity.FindClient(id)
|
||||
m = entity.FindClientByUID(id)
|
||||
|
||||
if m == nil {
|
||||
return fmt.Errorf("client %s not found", clean.LogQuote(id))
|
||||
|
|
|
@ -228,7 +228,7 @@ func (c *Config) ClientPublic() ClientConfig {
|
|||
|
||||
cfg := ClientConfig{
|
||||
Settings: c.PublicSettings(),
|
||||
ACL: acl.Resources.Grants(acl.RoleUnknown),
|
||||
ACL: acl.Resources.Grants(acl.RoleNone),
|
||||
Disable: ClientDisable{
|
||||
WebDAV: true,
|
||||
Settings: c.DisableSettings(),
|
||||
|
@ -683,12 +683,12 @@ func (c *Config) ClientRole(role acl.Role) ClientConfig {
|
|||
// 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, acl.RoleClient)
|
||||
cfg = c.ClientUser(false).ApplyACL(acl.Resources, 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.User().AclRole())
|
||||
cfg = c.ClientUser(false).ApplyACL(acl.Resources, sess.UserRole())
|
||||
cfg.Settings = c.SessionSettings(sess)
|
||||
} else {
|
||||
cfg = c.ClientPublic()
|
||||
|
|
|
@ -167,8 +167,8 @@ func TestConfig_ClientRoleConfig(t *testing.T) {
|
|||
|
||||
assert.Equal(t, expected, f)
|
||||
})
|
||||
t.Run("RoleUnknown", func(t *testing.T) {
|
||||
cfg := c.ClientRole(acl.RoleUnknown)
|
||||
t.Run("RoleNone", func(t *testing.T) {
|
||||
cfg := c.ClientRole(acl.RoleNone)
|
||||
f := cfg.Settings.Features
|
||||
|
||||
assert.NotEqual(t, adminFeatures, f)
|
||||
|
@ -355,7 +355,7 @@ func TestConfig_ClientSessionConfig(t *testing.T) {
|
|||
assert.True(t, f.Review)
|
||||
assert.False(t, f.Share)
|
||||
})
|
||||
t.Run("RoleUnknown", func(t *testing.T) {
|
||||
t.Run("RoleNone", func(t *testing.T) {
|
||||
sess := entity.SessionFixtures.Pointer("unauthorized")
|
||||
|
||||
cfg := c.ClientSession(sess)
|
||||
|
|
|
@ -7,7 +7,6 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/customize"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
|
@ -76,7 +75,7 @@ func (c *Config) SessionSettings(sess *entity.Session) *customize.Settings {
|
|||
}
|
||||
|
||||
if sess.NoUser() && sess.IsClient() {
|
||||
return c.Settings().ApplyACL(acl.Resources, acl.RoleClient).ApplyScope(sess.Scope())
|
||||
return c.Settings().ApplyACL(acl.Resources, sess.ClientRole()).ApplyScope(sess.Scope())
|
||||
}
|
||||
|
||||
user := sess.User()
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
|
@ -27,9 +28,10 @@ type Clients []Client
|
|||
type Client struct {
|
||||
ClientUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"-" yaml:"ClientUID"`
|
||||
UserUID string `gorm:"type:VARBINARY(42);index;default:'';" json:"UserUID" yaml:"UserUID,omitempty"`
|
||||
UserName string `gorm:"size:64;index;" json:"UserName" yaml:"UserName,omitempty"`
|
||||
UserName string `gorm:"size:200;index;" json:"UserName" yaml:"UserName,omitempty"`
|
||||
user *User `gorm:"-"`
|
||||
ClientName string `gorm:"size:200;" json:"ClientName" yaml:"ClientName,omitempty"`
|
||||
ClientRole string `gorm:"size:64;default:'';" json:"ClientRole" yaml:"ClientRole,omitempty"`
|
||||
ClientType string `gorm:"type:VARBINARY(16)" json:"ClientType" yaml:"ClientType,omitempty"`
|
||||
ClientURL string `gorm:"type:VARBINARY(255);default:'';column:client_url;" json:"ClientURL" yaml:"ClientURL,omitempty"`
|
||||
CallbackURL string `gorm:"type:VARBINARY(255);default:'';column:callback_url;" json:"CallbackURL" yaml:"CallbackURL,omitempty"`
|
||||
|
@ -54,6 +56,7 @@ func NewClient() *Client {
|
|||
return &Client{
|
||||
UserUID: "",
|
||||
ClientName: "",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
|
@ -77,8 +80,8 @@ func (m *Client) BeforeCreate(scope *gorm.Scope) error {
|
|||
return scope.SetColumn("ClientUID", m.ClientUID)
|
||||
}
|
||||
|
||||
// FindClient returns the matching client or nil if it was not found.
|
||||
func FindClient(uid string) *Client {
|
||||
// FindClientByUID returns the matching client or nil if it was not found.
|
||||
func FindClientByUID(uid string) *Client {
|
||||
if rnd.InvalidUID(uid, ClientUID) {
|
||||
return nil
|
||||
}
|
||||
|
@ -103,6 +106,36 @@ func (m *Client) HasUID() bool {
|
|||
return rnd.IsUID(m.ClientUID, ClientUID)
|
||||
}
|
||||
|
||||
// Name returns the client name string.
|
||||
func (m *Client) Name() string {
|
||||
return m.ClientName
|
||||
}
|
||||
|
||||
// SetRole sets the client role specified as string.
|
||||
func (m *Client) SetRole(role string) *Client {
|
||||
m.ClientRole = acl.ClientRoles[clean.Role(role)].String()
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// HasRole checks the client role specified as string.
|
||||
func (m *Client) HasRole(role acl.Role) bool {
|
||||
return m.AclRole() == role
|
||||
}
|
||||
|
||||
// AclRole returns the client role for ACL permission checks.
|
||||
func (m *Client) AclRole() acl.Role {
|
||||
if m == nil {
|
||||
return acl.RoleNone
|
||||
}
|
||||
|
||||
if role, ok := acl.ClientRoles[clean.Role(m.ClientRole)]; ok {
|
||||
return role
|
||||
}
|
||||
|
||||
return acl.RoleNone
|
||||
}
|
||||
|
||||
// User returns the related user account, if any.
|
||||
func (m *Client) User() *User {
|
||||
if m.user != nil {
|
||||
|
@ -133,6 +166,17 @@ func (m *Client) SetUser(u *User) *Client {
|
|||
return m
|
||||
}
|
||||
|
||||
// UserInfo returns user identification info.
|
||||
func (m *Client) UserInfo() string {
|
||||
if m.UserUID == "" {
|
||||
return ""
|
||||
} else if m.UserName != "" {
|
||||
return m.UserName
|
||||
}
|
||||
|
||||
return m.UserUID
|
||||
}
|
||||
|
||||
// Create new entity in the database.
|
||||
func (m *Client) Create() error {
|
||||
return Db().Create(m).Error
|
||||
|
@ -257,14 +301,7 @@ func (m *Client) UpdateLastActive() *Client {
|
|||
// NewSession creates a new client session.
|
||||
func (m *Client) NewSession(c *gin.Context) *Session {
|
||||
// Create, initialize, and return new session.
|
||||
sess := NewSession(m.AuthExpires, 0).SetContext(c)
|
||||
sess.AuthID = m.UID()
|
||||
sess.AuthProvider = authn.ProviderClient.String()
|
||||
sess.AuthMethod = m.Method().String()
|
||||
sess.AuthScope = m.Scope()
|
||||
sess.SetUser(m.User())
|
||||
|
||||
return sess
|
||||
return NewSession(m.AuthExpires, 0).SetContext(c).SetClient(m)
|
||||
}
|
||||
|
||||
// EnforceAuthTokenLimit deletes client sessions above the configured limit and returns the number of deleted sessions.
|
||||
|
@ -323,6 +360,10 @@ func (m *Client) SetFormValues(frm form.Client) *Client {
|
|||
m.ClientName = frm.Name()
|
||||
}
|
||||
|
||||
if frm.ClientRole != "" {
|
||||
m.SetRole(frm.ClientRole)
|
||||
}
|
||||
|
||||
if frm.AuthMethod != "" {
|
||||
m.AuthMethod = frm.Method().String()
|
||||
}
|
||||
|
|
|
@ -2,23 +2,22 @@ package entity
|
|||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// NewClientAccessToken returns a new access token session instance
|
||||
// that can be used to access the API with unregistered clients.
|
||||
func NewClientAccessToken(name string, lifetime int64, scope string, user *User) *Session {
|
||||
func NewClientAccessToken(clientName string, lifetime int64, scope string, user *User) *Session {
|
||||
sess := NewSession(lifetime, 0)
|
||||
|
||||
if name == "" {
|
||||
name = rnd.Name()
|
||||
if clientName == "" {
|
||||
clientName = rnd.Name()
|
||||
}
|
||||
|
||||
sess.AuthID = clean.Name(name)
|
||||
sess.AuthProvider = authn.ProviderClient.String()
|
||||
sess.AuthMethod = authn.MethodAccessToken.String()
|
||||
sess.AuthScope = clean.Scope(scope)
|
||||
sess.SetClientName(clientName)
|
||||
sess.SetProvider(authn.ProviderClient)
|
||||
sess.SetMethod(authn.MethodAccessToken)
|
||||
sess.SetScope(scope)
|
||||
|
||||
if user != nil {
|
||||
sess.SetUser(user)
|
||||
|
@ -30,8 +29,8 @@ func NewClientAccessToken(name string, lifetime int64, scope string, user *User)
|
|||
|
||||
// CreateClientAccessToken initializes and creates a new access token session
|
||||
// that can be used to access the API with unregistered clients.
|
||||
func CreateClientAccessToken(name string, lifetime int64, scope string, user *User) (*Session, error) {
|
||||
sess := NewClientAccessToken(name, lifetime, scope, user)
|
||||
func CreateClientAccessToken(clientName string, lifetime int64, scope string, user *User) (*Session, error) {
|
||||
sess := NewClientAccessToken(clientName, lifetime, scope, user)
|
||||
|
||||
if err := sess.Create(); err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package entity
|
||||
|
||||
import "github.com/photoprism/photoprism/pkg/authn"
|
||||
import (
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
)
|
||||
|
||||
type ClientMap map[string]Client
|
||||
|
||||
|
@ -27,6 +30,7 @@ var ClientFixtures = ClientMap{
|
|||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
ClientName: "Alice",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
|
@ -43,6 +47,7 @@ var ClientFixtures = ClientMap{
|
|||
UserName: UserFixtures.Pointer("bob").UserName,
|
||||
user: UserFixtures.Pointer("bob"),
|
||||
ClientName: "Bob",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientPublic,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
|
@ -59,6 +64,7 @@ var ClientFixtures = ClientMap{
|
|||
UserName: "",
|
||||
user: nil,
|
||||
ClientName: "Monitoring",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
|
@ -75,6 +81,7 @@ var ClientFixtures = ClientMap{
|
|||
UserName: "",
|
||||
user: nil,
|
||||
ClientName: "Unknown",
|
||||
ClientRole: acl.RoleNone.String(),
|
||||
ClientType: authn.ClientUnknown,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
|
@ -91,6 +98,7 @@ var ClientFixtures = ClientMap{
|
|||
UserName: "",
|
||||
user: nil,
|
||||
ClientName: "Deleted Monitoring",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
|
|
|
@ -22,7 +22,7 @@ func TestFindClient(t *testing.T) {
|
|||
t.Run("Alice", func(t *testing.T) {
|
||||
expected := ClientFixtures.Get("alice")
|
||||
|
||||
m := FindClient("cs5gfen1bgxz7s9i")
|
||||
m := FindClientByUID("cs5gfen1bgxz7s9i")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
|
@ -36,7 +36,7 @@ func TestFindClient(t *testing.T) {
|
|||
t.Run("Bob", func(t *testing.T) {
|
||||
expected := ClientFixtures.Get("bob")
|
||||
|
||||
m := FindClient("cs5gfsvbd7ejzn8m")
|
||||
m := FindClientByUID("cs5gfsvbd7ejzn8m")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
|
@ -50,7 +50,7 @@ func TestFindClient(t *testing.T) {
|
|||
t.Run("Metrics", func(t *testing.T) {
|
||||
expected := ClientFixtures.Get("metrics")
|
||||
|
||||
m := FindClient("cs5cpu17n6gj2qo5")
|
||||
m := FindClientByUID("cs5cpu17n6gj2qo5")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
|
@ -62,7 +62,7 @@ func TestFindClient(t *testing.T) {
|
|||
assert.NotEmpty(t, m.UpdatedAt)
|
||||
})
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
m := FindClient("123")
|
||||
m := FindClientByUID("123")
|
||||
assert.Nil(t, m)
|
||||
})
|
||||
}
|
||||
|
@ -161,7 +161,7 @@ func TestClient_Create(t *testing.T) {
|
|||
|
||||
func TestClient_Save(t *testing.T) {
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
c := FindClient("cs5cpu17n6gj2aaa")
|
||||
c := FindClientByUID("cs5cpu17n6gj2aaa")
|
||||
assert.Nil(t, c)
|
||||
|
||||
var m = Client{ClientName: "New Client", ClientUID: "cs5cpu17n6gj2aaa"}
|
||||
|
@ -169,7 +169,7 @@ func TestClient_Save(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
c = FindClient("cs5cpu17n6gj2aaa")
|
||||
c = FindClientByUID("cs5cpu17n6gj2aaa")
|
||||
|
||||
if c == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
|
@ -301,7 +301,7 @@ func TestClient_HasPassword(t *testing.T) {
|
|||
t.Run("Alice", func(t *testing.T) {
|
||||
expected := ClientFixtures.Get("alice")
|
||||
|
||||
m := FindClient("cs5gfen1bgxz7s9i")
|
||||
m := FindClientByUID("cs5gfen1bgxz7s9i")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
|
@ -320,7 +320,7 @@ func TestClient_HasPassword(t *testing.T) {
|
|||
t.Run("Metrics", func(t *testing.T) {
|
||||
expected := ClientFixtures.Get("metrics")
|
||||
|
||||
m := FindClient("cs5cpu17n6gj2qo5")
|
||||
m := FindClientByUID("cs5cpu17n6gj2qo5")
|
||||
|
||||
if m == nil {
|
||||
t.Fatal("result should not be nil")
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/i18n"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
|
@ -32,11 +33,14 @@ type Sessions []Session
|
|||
// Session represents a User session.
|
||||
type Session struct {
|
||||
ID string `gorm:"type:VARBINARY(2048);primary_key;auto_increment:false;" json:"-" yaml:"ID"`
|
||||
ClientIP string `gorm:"size:64;column:client_ip;index" json:"ClientIP" yaml:"ClientIP,omitempty"`
|
||||
UserUID string `gorm:"type:VARBINARY(42);index;default:'';" json:"UserUID" yaml:"UserUID,omitempty"`
|
||||
UserName string `gorm:"size:64;index;" json:"UserName" yaml:"UserName,omitempty"`
|
||||
user *User `gorm:"-"`
|
||||
authToken string `gorm:"-"`
|
||||
UserUID string `gorm:"type:VARBINARY(42);index;default:'';" json:"UserUID" yaml:"UserUID,omitempty"`
|
||||
UserName string `gorm:"size:200;index;" json:"UserName" yaml:"UserName,omitempty"`
|
||||
user *User `gorm:"-"`
|
||||
ClientUID string `gorm:"type:VARBINARY(42);index;default:'';" json:"ClientUID" yaml:"ClientUID,omitempty"`
|
||||
ClientName string `gorm:"size:200;default:'';" json:"ClientName" yaml:"ClientName,omitempty"`
|
||||
ClientIP string `gorm:"size:64;column:client_ip;index" json:"ClientIP" yaml:"ClientIP,omitempty"`
|
||||
client *Client `gorm:"-"`
|
||||
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"AuthProvider" yaml:"AuthProvider,omitempty"`
|
||||
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"AuthMethod" yaml:"AuthMethod,omitempty"`
|
||||
AuthDomain string `gorm:"type:VARBINARY(255);default:'';" json:"AuthDomain" yaml:"AuthDomain,omitempty"`
|
||||
|
@ -121,7 +125,7 @@ func DeleteClientSessions(clientUID string, authMethod authn.MethodType, limit i
|
|||
|
||||
found := Sessions{}
|
||||
|
||||
q := Db().Where("auth_id = ? AND auth_provider = ?", clientUID, authn.ProviderClient.String())
|
||||
q := Db().Where("client_uid = ?", clientUID)
|
||||
|
||||
if !authMethod.IsDefault() {
|
||||
q = q.Where("auth_method = ?", authMethod.String())
|
||||
|
@ -278,9 +282,100 @@ func (m *Session) BeforeCreate(scope *gorm.Scope) error {
|
|||
return scope.SetColumn("ID", m.ID)
|
||||
}
|
||||
|
||||
// User returns the session's user.
|
||||
// SetClient updates the client of this session.
|
||||
func (m *Session) SetClient(c *Client) *Session {
|
||||
if c == nil {
|
||||
return m
|
||||
}
|
||||
|
||||
m.client = c
|
||||
m.ClientUID = c.UID()
|
||||
m.ClientName = c.ClientName
|
||||
m.AuthProvider = authn.ProviderClient.String()
|
||||
m.AuthMethod = c.Method().String()
|
||||
m.AuthScope = c.Scope()
|
||||
m.SetUser(c.User())
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// SetClientName changes the session's client name.
|
||||
func (m *Session) SetClientName(s string) *Session {
|
||||
if s == "" {
|
||||
return m
|
||||
}
|
||||
|
||||
m.ClientName = clean.Name(s)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Client returns the session's client.
|
||||
func (m *Session) Client() *Client {
|
||||
if m == nil {
|
||||
return &Client{}
|
||||
} else if m.client != nil {
|
||||
return m.client
|
||||
} else if c := FindClientByUID(m.ClientUID); c != nil {
|
||||
m.SetClient(c)
|
||||
return m.client
|
||||
}
|
||||
|
||||
return &Client{
|
||||
UserUID: m.UserUID,
|
||||
UserName: m.UserName,
|
||||
ClientUID: m.ClientUID,
|
||||
ClientName: m.ClientName,
|
||||
ClientRole: m.ClientRole().String(),
|
||||
AuthScope: m.Scope(),
|
||||
AuthMethod: m.AuthMethod,
|
||||
}
|
||||
}
|
||||
|
||||
// ClientRole returns the session's client ACL role.
|
||||
func (m *Session) ClientRole() acl.Role {
|
||||
if m.HasClient() {
|
||||
return m.Client().AclRole()
|
||||
} else if m.IsClient() {
|
||||
return acl.RoleClient
|
||||
}
|
||||
|
||||
return acl.RoleNone
|
||||
}
|
||||
|
||||
// ClientInfo returns the session's client identifier string.
|
||||
func (m *Session) ClientInfo() string {
|
||||
if m.HasClient() {
|
||||
return m.Client().Name()
|
||||
}
|
||||
|
||||
return m.ClientName
|
||||
}
|
||||
|
||||
// HasClient checks if a client entity is assigned to the session.
|
||||
func (m *Session) HasClient() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.ClientUID != ""
|
||||
}
|
||||
|
||||
// NoClient if this session has no client entity assigned.
|
||||
func (m *Session) NoClient() bool {
|
||||
return !m.HasClient()
|
||||
}
|
||||
|
||||
// IsClient checks if this session authenticates an API client.
|
||||
func (m *Session) IsClient() bool {
|
||||
return authn.Provider(m.AuthProvider).IsClient()
|
||||
}
|
||||
|
||||
// User returns the session's user entity.
|
||||
func (m *Session) User() *User {
|
||||
if m.user != nil {
|
||||
if m == nil {
|
||||
return &User{}
|
||||
} else if m.user != nil {
|
||||
return m.user
|
||||
} else if m.UserUID == "" {
|
||||
return &User{}
|
||||
|
@ -294,6 +389,54 @@ func (m *Session) User() *User {
|
|||
return &User{}
|
||||
}
|
||||
|
||||
// UserRole returns the session's user ACL role.
|
||||
func (m *Session) UserRole() acl.Role {
|
||||
return m.User().AclRole()
|
||||
}
|
||||
|
||||
// UserInfo returns the session's user information.
|
||||
func (m *Session) UserInfo() string {
|
||||
name := m.Username()
|
||||
|
||||
if name != "" {
|
||||
return name
|
||||
}
|
||||
|
||||
return m.UserRole().String()
|
||||
}
|
||||
|
||||
// SetUser updates the user entity of this session.
|
||||
func (m *Session) SetUser(u *User) *Session {
|
||||
if u == nil {
|
||||
return m
|
||||
}
|
||||
|
||||
// Update user.
|
||||
m.user = u
|
||||
m.UserUID = u.UserUID
|
||||
m.UserName = u.UserName
|
||||
|
||||
// Update tokens.
|
||||
m.SetPreviewToken(u.PreviewToken)
|
||||
m.SetDownloadToken(u.DownloadToken)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// HasUser checks if a user entity is assigned to the session.
|
||||
func (m *Session) HasUser() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.UserUID != ""
|
||||
}
|
||||
|
||||
// NoUser checks if this session has no user entity assigned.
|
||||
func (m *Session) NoUser() bool {
|
||||
return !m.HasUser()
|
||||
}
|
||||
|
||||
// RefreshUser updates the cached user data.
|
||||
func (m *Session) RefreshUser() *Session {
|
||||
// Remove user if uid is nil.
|
||||
|
@ -310,24 +453,6 @@ func (m *Session) RefreshUser() *Session {
|
|||
return m
|
||||
}
|
||||
|
||||
// SetUser updates the session's user.
|
||||
func (m *Session) SetUser(u *User) *Session {
|
||||
if u == nil {
|
||||
return m
|
||||
}
|
||||
|
||||
// Update user.
|
||||
m.user = u
|
||||
m.UserUID = u.UserUID
|
||||
m.UserName = u.UserName
|
||||
|
||||
// Update tokens.
|
||||
m.SetPreviewToken(u.PreviewToken)
|
||||
m.SetDownloadToken(u.DownloadToken)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// Username returns the login name.
|
||||
func (m *Session) Username() string {
|
||||
return m.UserName
|
||||
|
@ -409,11 +534,6 @@ func (m *Session) SetProvider(provider authn.ProviderType) *Session {
|
|||
return m
|
||||
}
|
||||
|
||||
// IsClient checks whether this session is used to authenticate an API client.
|
||||
func (m *Session) IsClient() bool {
|
||||
return authn.Provider(m.AuthProvider).IsClient()
|
||||
}
|
||||
|
||||
// ChangePassword changes the password of the current user.
|
||||
func (m *Session) ChangePassword(newPw string) (err error) {
|
||||
u := m.User()
|
||||
|
@ -605,20 +725,6 @@ func (m *Session) HasShares() bool {
|
|||
}
|
||||
}
|
||||
|
||||
// NoUser checks if this session has no specific user assigned.
|
||||
func (m *Session) NoUser() bool {
|
||||
return !m.HasUser()
|
||||
}
|
||||
|
||||
// HasUser checks if a user account is assigned to the session.
|
||||
func (m *Session) HasUser() bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.UserUID != ""
|
||||
}
|
||||
|
||||
// HasRegisteredUser checks if the session belongs to a registered user.
|
||||
func (m *Session) HasRegisteredUser() bool {
|
||||
if !m.HasUser() {
|
||||
|
|
|
@ -44,7 +44,7 @@ var SessionFixtures = SessionMap{
|
|||
AuthScope: clean.Scope("*"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "alice_token",
|
||||
ClientName: "alice_token",
|
||||
LastActive: -1,
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
|
@ -59,7 +59,7 @@ var SessionFixtures = SessionMap{
|
|||
AuthScope: clean.Scope("*"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "alice_token_personal",
|
||||
ClientName: "alice_token_personal",
|
||||
LastActive: -1,
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
|
@ -74,7 +74,7 @@ var SessionFixtures = SessionMap{
|
|||
AuthScope: clean.Scope("webdav"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "alice_token_webdav",
|
||||
ClientName: "alice_token_webdav",
|
||||
LastActive: -1,
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
|
@ -89,7 +89,7 @@ var SessionFixtures = SessionMap{
|
|||
AuthScope: clean.Scope("metrics photos albums videos"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "alice_token_scope",
|
||||
ClientName: "alice_token_scope",
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
|
@ -140,7 +140,7 @@ var SessionFixtures = SessionMap{
|
|||
AuthScope: clean.Scope("metrics"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "visitor_token_metrics",
|
||||
ClientName: "visitor_token_metrics",
|
||||
user: &Visitor,
|
||||
UserUID: Visitor.UserUID,
|
||||
UserName: Visitor.UserName,
|
||||
|
@ -155,6 +155,23 @@ var SessionFixtures = SessionMap{
|
|||
UserUID: UserFixtures.Pointer("friend").UserUID,
|
||||
UserName: UserFixtures.Pointer("friend").UserName,
|
||||
},
|
||||
"client_metrics": {
|
||||
authToken: "9d8b8801ffa23eb52e08ca7766283799ddfd8dd368212345",
|
||||
ID: rnd.SessionID("9d8b8801ffa23eb52e08ca7766283799ddfd8dd368212345"),
|
||||
RefID: "sessgh612345",
|
||||
SessTimeout: 0,
|
||||
SessExpires: UnixTime() + UnixWeek,
|
||||
AuthScope: clean.Scope("metrics"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
ClientUID: ClientFixtures.Get("metrics").ClientUID,
|
||||
ClientName: ClientFixtures.Get("metrics").ClientName,
|
||||
user: nil,
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
PreviewToken: "py212345",
|
||||
DownloadToken: "vgl12345",
|
||||
},
|
||||
"token_metrics": {
|
||||
authToken: "9d8b8801ffa23eb52e08ca7766283799ddfd8dd368208a9b",
|
||||
ID: rnd.SessionID("9d8b8801ffa23eb52e08ca7766283799ddfd8dd368208a9b"),
|
||||
|
@ -164,7 +181,7 @@ var SessionFixtures = SessionMap{
|
|||
AuthScope: clean.Scope("metrics"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "token_metrics",
|
||||
ClientName: "token_metrics",
|
||||
user: nil,
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
|
@ -180,7 +197,7 @@ var SessionFixtures = SessionMap{
|
|||
AuthScope: clean.Scope("settings"),
|
||||
AuthProvider: authn.ProviderClient.String(),
|
||||
AuthMethod: authn.MethodAccessToken.String(),
|
||||
AuthID: "token_settings",
|
||||
ClientName: "token_settings",
|
||||
user: nil,
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
|
|
|
@ -125,7 +125,8 @@ func AuthLocal(user *User, f form.Login, m *Session, c *gin.Context) (authn.Prov
|
|||
m.Status = http.StatusUnauthorized
|
||||
return authn.ProviderNone, i18n.Error(i18n.ErrInvalidCredentials)
|
||||
} else {
|
||||
m.SetAuthID(authSess.AuthID)
|
||||
m.ClientUID = authSess.ClientUID
|
||||
m.ClientName = authSess.ClientName
|
||||
m.SetScope(authSess.Scope())
|
||||
m.SetMethod(authn.MethodSession)
|
||||
event.AuditInfo([]string{clientIp, "session %s", "login as %s with auth secret", "succeeded"}, m.RefID, clean.LogQuote(userName))
|
||||
|
|
|
@ -5,11 +5,11 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/acl"
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/header"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
|
@ -99,22 +99,23 @@ func TestDeleteExpiredSessions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestDeleteClientSessions(t *testing.T) {
|
||||
// Test client UID.
|
||||
clientUID := "cs5gfen1bgx00000"
|
||||
|
||||
// Create new test client.
|
||||
client := NewClient()
|
||||
client.ClientUID = "cs5gfen1bgx00000"
|
||||
|
||||
// Make sure no sessions exist yet and test missing arguments.
|
||||
assert.Equal(t, 0, DeleteClientSessions("", "", -1))
|
||||
assert.Equal(t, 0, DeleteClientSessions(clientUID, authn.MethodOAuth2, -1))
|
||||
assert.Equal(t, 0, DeleteClientSessions(clientUID, authn.MethodOAuth2, 0))
|
||||
assert.Equal(t, 0, DeleteClientSessions("", authn.MethodDefault, 0))
|
||||
|
||||
// Create 10 client sessions.
|
||||
// Create 10 test client sessions.
|
||||
for i := 0; i < 10; i++ {
|
||||
sess := NewSession(3600, 0)
|
||||
sess.SetClientIP(UnknownIP)
|
||||
sess.AuthID = clientUID
|
||||
sess.AuthProvider = authn.ProviderClient.String()
|
||||
sess.AuthMethod = authn.MethodOAuth2.String()
|
||||
sess.AuthScope = "*"
|
||||
sess.SetClient(client)
|
||||
|
||||
if err := sess.Save(); err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -313,24 +314,140 @@ func TestSession_Updates(t *testing.T) {
|
|||
m := FindSessionByRefID("sessxkkcabcd")
|
||||
assert.Equal(t, "alice", m.UserName)
|
||||
|
||||
m.Updates(Session{UserName: "anton"})
|
||||
if err := m.Updates(Session{UserName: "anton"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, "anton", m.UserName)
|
||||
}
|
||||
|
||||
func TestSession_Client(t *testing.T) {
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
m := FindSessionByRefID("sessxkkcabcd")
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.UserUID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.User().UserUID)
|
||||
assert.Equal(t, "", m.Client().ClientUID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.Client().UserUID)
|
||||
assert.Equal(t, acl.RoleNone, m.Client().AclRole())
|
||||
assert.Equal(t, acl.RoleNone, m.ClientRole())
|
||||
})
|
||||
t.Run("AliceTokenPersonal", func(t *testing.T) {
|
||||
m := SessionFixtures.Get("alice_token_personal")
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.UserUID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.User().UserUID)
|
||||
assert.Equal(t, "", m.Client().ClientUID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.Client().UserUID)
|
||||
assert.Equal(t, acl.RoleClient, m.Client().AclRole())
|
||||
assert.Equal(t, acl.RoleClient, m.ClientRole())
|
||||
})
|
||||
t.Run("ClientMetrics", func(t *testing.T) {
|
||||
m := SessionFixtures.Get("client_metrics")
|
||||
assert.Equal(t, "", m.UserUID)
|
||||
assert.Equal(t, "", m.User().UserUID)
|
||||
assert.Equal(t, "cs5cpu17n6gj2qo5", m.Client().ClientUID)
|
||||
assert.Equal(t, "", m.Client().UserUID)
|
||||
assert.Equal(t, acl.RoleClient, m.Client().AclRole())
|
||||
assert.Equal(t, acl.RoleClient, m.ClientRole())
|
||||
})
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
m := &Session{}
|
||||
assert.Equal(t, "", m.UserUID)
|
||||
assert.Equal(t, "", m.User().UserUID)
|
||||
assert.Equal(t, "", m.Client().ClientUID)
|
||||
assert.Equal(t, "", m.Client().UserUID)
|
||||
assert.Equal(t, acl.RoleNone, m.Client().AclRole())
|
||||
assert.Equal(t, acl.RoleNone, m.ClientRole())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_ClientRole(t *testing.T) {
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
m := SessionFixtures.Get("alice")
|
||||
assert.Equal(t, acl.RoleNone, m.ClientRole())
|
||||
})
|
||||
t.Run("AliceTokenPersonal", func(t *testing.T) {
|
||||
m := SessionFixtures.Get("alice_token_personal")
|
||||
assert.Equal(t, acl.RoleClient, m.ClientRole())
|
||||
})
|
||||
t.Run("TokenMetrics", func(t *testing.T) {
|
||||
m := SessionFixtures.Get("token_metrics")
|
||||
assert.Equal(t, acl.RoleClient, m.ClientRole())
|
||||
})
|
||||
t.Run("TokenSettings", func(t *testing.T) {
|
||||
m := SessionFixtures.Get("token_settings")
|
||||
assert.Equal(t, acl.RoleClient, m.ClientRole())
|
||||
})
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
m := &Session{}
|
||||
assert.Equal(t, acl.RoleNone, m.ClientRole())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_SetClient(t *testing.T) {
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
m := SessionFixtures.Get("alice")
|
||||
assert.Equal(t, acl.RoleNone, m.ClientRole())
|
||||
assert.Equal(t, "", m.Client().ClientUID)
|
||||
m.SetClient(ClientFixtures.Pointer("alice"))
|
||||
assert.Equal(t, acl.RoleClient, m.ClientRole())
|
||||
assert.Equal(t, "cs5gfen1bgxz7s9i", m.Client().ClientUID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_SetClientName(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
m := SessionFixtures.Get("alice_token_personal")
|
||||
assert.Equal(t, "", m.ClientUID)
|
||||
assert.Equal(t, "alice_token_personal", m.ClientName)
|
||||
assert.Equal(t, "alice_token_personal", m.ClientInfo())
|
||||
m.SetClientName("Foo Bar!")
|
||||
assert.Equal(t, "", m.ClientUID)
|
||||
assert.Equal(t, "Foo Bar!", m.ClientName)
|
||||
assert.Equal(t, "Foo Bar!", m.ClientInfo())
|
||||
m.SetClientName("")
|
||||
assert.Equal(t, "Foo Bar!", m.ClientName)
|
||||
assert.Equal(t, "Foo Bar!", m.ClientInfo())
|
||||
})
|
||||
t.Run("setNewID", func(t *testing.T) {
|
||||
m := NewSession(0, 0)
|
||||
assert.Equal(t, "", m.ClientUID)
|
||||
assert.Equal(t, "", m.ClientName)
|
||||
assert.Equal(t, "", m.ClientInfo())
|
||||
m.SetClientName("Foo Bar!")
|
||||
assert.Equal(t, "", m.ClientUID)
|
||||
assert.Equal(t, "Foo Bar!", m.ClientName)
|
||||
assert.Equal(t, "Foo Bar!", m.ClientInfo())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_User(t *testing.T) {
|
||||
t.Run("alice", func(t *testing.T) {
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
m := FindSessionByRefID("sessxkkcabcd")
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", m.User().UserUID)
|
||||
})
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
m := &Session{}
|
||||
assert.Equal(t, "", m.User().UserUID)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_UserRole(t *testing.T) {
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
m := FindSessionByRefID("sessxkkcabcd")
|
||||
assert.Equal(t, acl.RoleAdmin, m.UserRole())
|
||||
})
|
||||
t.Run("Bob", func(t *testing.T) {
|
||||
m := FindSessionByRefID("sessxkkcabce")
|
||||
assert.Equal(t, acl.RoleAdmin, m.UserRole())
|
||||
})
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
m := &Session{}
|
||||
assert.Equal(t, acl.RoleNone, m.UserRole())
|
||||
})
|
||||
}
|
||||
|
||||
func TestSession_RefreshUser(t *testing.T) {
|
||||
t.Run("bob", func(t *testing.T) {
|
||||
t.Run("Bob", func(t *testing.T) {
|
||||
m := FindSessionByRefID("sessxkkcabce")
|
||||
|
||||
assert.Equal(t, "bob", m.Username())
|
||||
|
@ -367,7 +484,7 @@ func TestSession_AuthInfo(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSession_SetAuthID(t *testing.T) {
|
||||
t.Run("emptyID", func(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
|
@ -378,7 +495,7 @@ func TestSession_SetAuthID(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "test-session-auth-id", m.AuthID)
|
||||
})
|
||||
t.Run("setNewID", func(t *testing.T) {
|
||||
t.Run("New", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
|
@ -392,7 +509,7 @@ func TestSession_SetAuthID(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSession_SetScope(t *testing.T) {
|
||||
t.Run("emptyScope", func(t *testing.T) {
|
||||
t.Run("EmptyScope", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
|
@ -403,7 +520,7 @@ func TestSession_SetScope(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "*", m.AuthScope)
|
||||
})
|
||||
t.Run("setNewScope", func(t *testing.T) {
|
||||
t.Run("NewScope", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
|
@ -417,7 +534,7 @@ func TestSession_SetScope(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSession_SetMethod(t *testing.T) {
|
||||
t.Run("emptyMethod", func(t *testing.T) {
|
||||
t.Run("EmptyMethod", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
|
@ -428,7 +545,7 @@ func TestSession_SetMethod(t *testing.T) {
|
|||
|
||||
assert.Equal(t, "Access Token", m.AuthMethod)
|
||||
})
|
||||
t.Run("setNewMethod", func(t *testing.T) {
|
||||
t.Run("NewMethod", func(t *testing.T) {
|
||||
s := &Session{
|
||||
UserName: "test",
|
||||
RefID: "sessxkkcxxxz",
|
||||
|
|
|
@ -48,7 +48,7 @@ type User struct {
|
|||
UserUID string `gorm:"type:VARBINARY(42);column:user_uid;unique_index;" json:"UID" yaml:"UID"`
|
||||
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"AuthProvider" yaml:"AuthProvider,omitempty"`
|
||||
AuthID string `gorm:"type:VARBINARY(255);index;default:'';" json:"AuthID" yaml:"AuthID,omitempty"`
|
||||
UserName string `gorm:"size:255;index;" json:"Name" yaml:"Name,omitempty"`
|
||||
UserName string `gorm:"size:200;index;" json:"Name" yaml:"Name,omitempty"`
|
||||
DisplayName string `gorm:"size:200;" json:"DisplayName" yaml:"DisplayName,omitempty"`
|
||||
UserEmail string `gorm:"size:255;index;" json:"Email" yaml:"Email,omitempty"`
|
||||
BackupEmail string `gorm:"size:255;" json:"BackupEmail,omitempty" yaml:"BackupEmail,omitempty"`
|
||||
|
@ -627,9 +627,9 @@ func (m *User) SetRole(role string) *User {
|
|||
|
||||
switch role {
|
||||
case "", "0", "false", "nil", "null", "nan":
|
||||
m.UserRole = acl.RoleUnknown.String()
|
||||
m.UserRole = acl.RoleNone.String()
|
||||
default:
|
||||
m.UserRole = acl.ValidRoles[role].String()
|
||||
m.UserRole = acl.UserRoles[role].String()
|
||||
}
|
||||
|
||||
return m
|
||||
|
@ -648,11 +648,11 @@ func (m *User) AclRole() acl.Role {
|
|||
case m.SuperAdmin:
|
||||
return acl.RoleAdmin
|
||||
case role == "":
|
||||
return acl.RoleUnknown
|
||||
return acl.RoleNone
|
||||
case m.UserName == "":
|
||||
return acl.RoleVisitor
|
||||
default:
|
||||
return acl.ValidRoles[role]
|
||||
return acl.UserRoles[role]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -754,7 +754,7 @@ func (m *User) IsUnknown() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
return !rnd.IsUID(m.UserUID, UserUID) || m.ID == UnknownUser.ID || m.UserUID == UnknownUser.UserUID || m.HasRole(acl.RoleUnknown)
|
||||
return !rnd.IsUID(m.UserUID, UserUID) || m.ID == UnknownUser.ID || m.UserUID == UnknownUser.UserUID || m.HasRole(acl.RoleNone)
|
||||
}
|
||||
|
||||
// DeleteSessions deletes all active user sessions except those passed as argument.
|
||||
|
@ -867,7 +867,7 @@ func (m *User) Validate() (err error) {
|
|||
}
|
||||
|
||||
// Check user role.
|
||||
if acl.ValidRoles[m.UserRole] == "" {
|
||||
if acl.UserRoles[m.UserRole] == "" {
|
||||
return fmt.Errorf("user role %s is invalid", clean.LogQuote(m.UserRole))
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ var UnknownUser = User{
|
|||
UserUID: "u000000000000001",
|
||||
UserName: "",
|
||||
AuthProvider: authn.ProviderNone.String(),
|
||||
UserRole: acl.RoleUnknown.String(),
|
||||
UserRole: acl.RoleNone.String(),
|
||||
CanLogin: false,
|
||||
WebDAV: false,
|
||||
CanInvite: false,
|
||||
|
|
|
@ -128,7 +128,7 @@ var UserFixtures = UserMap{
|
|||
UserUID: "uriku0138hqql4bz",
|
||||
UserName: "jens.mander",
|
||||
UserEmail: "jens.mander@microsoft.com",
|
||||
UserRole: acl.RoleUnknown.String(),
|
||||
UserRole: acl.RoleNone.String(),
|
||||
AuthProvider: authn.ProviderNone.String(),
|
||||
SuperAdmin: false,
|
||||
DisplayName: "Jens Mander",
|
||||
|
|
|
@ -39,7 +39,7 @@ func TestUserMap_Pointer(t *testing.T) {
|
|||
r := UserFixtures.Pointer("monstera")
|
||||
assert.Equal(t, "", r.UserName)
|
||||
assert.Equal(t, "", r.Email())
|
||||
assert.Equal(t, acl.RoleUnknown, r.AclRole())
|
||||
assert.Equal(t, acl.RoleNone, r.AclRole())
|
||||
assert.IsType(t, &User{}, r)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -739,7 +739,7 @@ func TestUser_AclRole(t *testing.T) {
|
|||
})
|
||||
t.Run("Unauthorized", func(t *testing.T) {
|
||||
p := User{ID: 8, UserUID: "u000000000000008", UserName: "Hanna", DisplayName: ""}
|
||||
assert.Equal(t, acl.RoleUnknown, p.AclRole())
|
||||
assert.Equal(t, acl.RoleNone, p.AclRole())
|
||||
assert.False(t, p.IsAdmin())
|
||||
assert.False(t, p.IsVisitor())
|
||||
})
|
||||
|
@ -943,7 +943,7 @@ func TestDeleteUser(t *testing.T) {
|
|||
UserName: "thomasdel2",
|
||||
UserEmail: "thomasdel2@example.com",
|
||||
DisplayName: "Thomas Delete 2",
|
||||
UserRole: acl.RoleUnknown.String(),
|
||||
UserRole: acl.RoleNone.String(),
|
||||
}
|
||||
|
||||
err := u.Delete()
|
||||
|
|
|
@ -13,6 +13,7 @@ type Client struct {
|
|||
UserUID string `json:"UserUID,omitempty" yaml:"UserUID,omitempty"`
|
||||
UserName string `gorm:"size:64;index;" json:"UserName" yaml:"UserName,omitempty"`
|
||||
ClientName string `json:"ClientName,omitempty" yaml:"ClientName,omitempty"`
|
||||
ClientRole string `json:"ClientRole,omitempty" yaml:"ClientRole,omitempty"`
|
||||
AuthMethod string `json:"AuthMethod,omitempty" yaml:"AuthMethod,omitempty"`
|
||||
AuthScope string `json:"AuthScope,omitempty" yaml:"AuthScope,omitempty"`
|
||||
AuthExpires int64 `json:"AuthExpires,omitempty" yaml:"AuthExpires,omitempty"`
|
||||
|
@ -39,6 +40,7 @@ func NewClientFromCli(ctx *cli.Context) Client {
|
|||
f := NewClient()
|
||||
|
||||
f.ClientName = clean.Name(ctx.String("name"))
|
||||
f.ClientRole = clean.Name(ctx.String("role"))
|
||||
f.AuthScope = clean.Scope(ctx.String("scope"))
|
||||
f.AuthMethod = authn.Method(ctx.String("method")).String()
|
||||
|
||||
|
@ -60,6 +62,11 @@ func (f *Client) Name() string {
|
|||
return clean.Name(f.ClientName)
|
||||
}
|
||||
|
||||
// Role returns the sanitized client role.
|
||||
func (f *Client) Role() string {
|
||||
return clean.Role(f.ClientRole)
|
||||
}
|
||||
|
||||
// Method returns the sanitized auth method name.
|
||||
func (f *Client) Method() authn.MethodType {
|
||||
return authn.Method(f.AuthMethod)
|
||||
|
|
|
@ -103,25 +103,25 @@ func WebDAVAuth(conf *config.Config) gin.HandlerFunc {
|
|||
} else if sess.IsClient() && !sess.HasScope(acl.ResourceWebDAV.String()) {
|
||||
// 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.AuthID), sess.RefID, clean.LogQuote(user.Username()))
|
||||
event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.ClientInfo()), sess.RefID, clean.LogQuote(user.Username()))
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
} else if !user.CanUseWebDAV() {
|
||||
// Log warning if WebDAV is disabled for this account.
|
||||
message := "webdav access is disabled"
|
||||
event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.AuthID), sess.RefID, clean.LogQuote(user.Username()))
|
||||
event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.ClientInfo()), sess.RefID, clean.LogQuote(user.Username()))
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
} else if username != "" && !strings.EqualFold(clean.Username(username), user.Username()) {
|
||||
// Log warning if WebDAV is disabled for this account.
|
||||
message := "basic auth username does not match"
|
||||
event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.AuthID), sess.RefID, clean.LogQuote(user.Username()))
|
||||
event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.ClientInfo()), sess.RefID, clean.LogQuote(user.Username()))
|
||||
WebDAVAbortUnauthorized(c)
|
||||
return
|
||||
} else if err := os.MkdirAll(filepath.Join(conf.OriginalsPath(), user.GetUploadPath()), fs.ModeDir); err != nil {
|
||||
// Log warning if upload path could not be created.
|
||||
message := "failed to create user upload path"
|
||||
event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.AuthID), sess.RefID, clean.LogQuote(user.Username()))
|
||||
event.AuditWarn([]string{clientIp, "client %s", "session %s", "access webdav as %s", message}, clean.Log(sess.ClientInfo()), sess.RefID, clean.LogQuote(user.Username()))
|
||||
WebDAVAbortServerError(c)
|
||||
return
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue