Auth: Refactor sessions API and model #98
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
9db0424c05
commit
0a5dce5aeb
|
@ -471,7 +471,7 @@
|
|||
<v-list-tile v-if="canManageUsers" :to="{ path: '/admin/users' }" :exact="false" class="nav-admin-users" @click.stop="">
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title :class="`menu-item ${rtl ? '--rtl' : ''}`">
|
||||
<translate>Users</translate>
|
||||
<translate>Authentication</translate>
|
||||
</v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</v-list-tile>
|
||||
|
|
|
@ -5,9 +5,12 @@ import (
|
|||
|
||||
"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/get"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// DeleteSession deletes an existing client session (logout).
|
||||
|
@ -17,6 +20,7 @@ func DeleteSession(router *gin.RouterGroup) {
|
|||
router.DELETE("/session/:id", func(c *gin.Context) {
|
||||
id := clean.ID(c.Param("id"))
|
||||
|
||||
// Abort if ID is missing.
|
||||
if id == "" {
|
||||
AbortBadRequest(c)
|
||||
return
|
||||
|
@ -25,6 +29,23 @@ func DeleteSession(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
// Find session by reference ID.
|
||||
if !rnd.IsRefID(id) {
|
||||
// Do nothing.
|
||||
} else if s := Session(SessionID(c)); s == nil {
|
||||
entity.SessionStatusUnauthorized().Abort(c)
|
||||
return
|
||||
} else if !acl.Resources.AllowAll(acl.ResourceUsers, s.User().AclRole(), acl.Permissions{acl.AccessAll, acl.ActionManage}) {
|
||||
s.Abort(c)
|
||||
return
|
||||
} else if ref := entity.FindSessionByRefID(id); ref == nil {
|
||||
AbortNotFound(c)
|
||||
return
|
||||
} else {
|
||||
id = ref.ID
|
||||
}
|
||||
|
||||
// Delete session by ID.
|
||||
if err := get.Session().Delete(id); err != nil {
|
||||
event.AuditErr([]string{ClientIP(c), "session %s"}, err)
|
||||
} else {
|
|
@ -29,33 +29,33 @@ type Sessions []Session
|
|||
|
||||
// Session represents a User session.
|
||||
type Session struct {
|
||||
ID string `gorm:"type:VARBINARY(2048);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
|
||||
ClientIP string `gorm:"size:64;column:client_ip;index" json:"-" yaml:"ClientIP,omitempty"`
|
||||
UserUID string `gorm:"type:VARBINARY(42);index;default:'';" json:"UserUID,omitempty" yaml:"UserUID,omitempty"`
|
||||
UserName string `gorm:"size:64;index;" json:"UserName,omitempty" yaml:"UserName,omitempty"`
|
||||
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:"-"`
|
||||
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"-" yaml:"AuthProvider,omitempty"`
|
||||
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"-" yaml:"AuthMethod,omitempty"`
|
||||
AuthDomain string `gorm:"type:VARBINARY(255);default:'';" json:"-" yaml:"AuthDomain,omitempty"`
|
||||
AuthID string `gorm:"type:VARBINARY(128);index;default:'';" json:"-" yaml:"AuthID,omitempty"`
|
||||
AuthScope string `gorm:"size:1024;default:'';" json:"-" yaml:"AuthScope,omitempty"`
|
||||
LastActive int64 `json:"LastActive,omitempty" yaml:"LastActive,omitempty"`
|
||||
SessExpires int64 `gorm:"index" json:"Expires,omitempty" yaml:"Expires,omitempty"`
|
||||
SessTimeout int64 `json:"Timeout,omitempty" yaml:"Timeout,omitempty"`
|
||||
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"`
|
||||
AuthID string `gorm:"type:VARBINARY(128);index;default:'';" json:"AuthID" yaml:"AuthID,omitempty"`
|
||||
AuthScope string `gorm:"size:1024;default:'';" json:"AuthScope" yaml:"AuthScope,omitempty"`
|
||||
LastActive int64 `json:"LastActive" yaml:"LastActive,omitempty"`
|
||||
SessExpires int64 `gorm:"index" json:"Expires" yaml:"Expires,omitempty"`
|
||||
SessTimeout int64 `json:"Timeout" yaml:"Timeout,omitempty"`
|
||||
PreviewToken string `gorm:"type:VARBINARY(64);column:preview_token;default:'';" json:"-" yaml:"-"`
|
||||
DownloadToken string `gorm:"type:VARBINARY(64);column:download_token;default:'';" json:"-" yaml:"-"`
|
||||
AccessToken string `gorm:"type:VARBINARY(4096);column:access_token;default:'';" json:"-" yaml:"-"`
|
||||
RefreshToken string `gorm:"type:VARBINARY(512);column:refresh_token;default:'';" json:"-" yaml:"-"`
|
||||
IdToken string `gorm:"type:VARBINARY(1024);column:id_token;default:'';" json:"IdToken,omitempty" yaml:"IdToken,omitempty"`
|
||||
UserAgent string `gorm:"size:512;" json:"-" yaml:"UserAgent,omitempty"`
|
||||
DataJSON json.RawMessage `gorm:"type:VARBINARY(4096);" json:"Data,omitempty" yaml:"Data,omitempty"`
|
||||
UserAgent string `gorm:"size:512;" json:"UserAgent" yaml:"UserAgent,omitempty"`
|
||||
DataJSON json.RawMessage `gorm:"type:VARBINARY(4096);" json:"-" yaml:"Data,omitempty"`
|
||||
data *SessionData `gorm:"-"`
|
||||
RefID string `gorm:"type:VARBINARY(16);default:'';" json:"-" yaml:"-"`
|
||||
LoginIP string `gorm:"size:64;column:login_ip" json:"-" yaml:"-"`
|
||||
LoginAt time.Time `json:"-" yaml:"-"`
|
||||
RefID string `gorm:"type:VARBINARY(16);default:'';" json:"ID" yaml:"-"`
|
||||
LoginIP string `gorm:"size:64;column:login_ip" json:"LoginIP" yaml:"-"`
|
||||
LoginAt time.Time `json:"LoginAt" yaml:"-"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"UpdatedAt"`
|
||||
Status int `gorm:"-" json:"Status,omitempty" yaml:"-"`
|
||||
Status int `gorm:"-" json:"Status" yaml:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the entity table name.
|
||||
|
@ -125,6 +125,22 @@ func SessionStatusForbidden() *Session {
|
|||
return &Session{Status: http.StatusForbidden}
|
||||
}
|
||||
|
||||
// FindSessionByRefID finds an existing session by ref ID.
|
||||
func FindSessionByRefID(refId string) *Session {
|
||||
if !rnd.IsRefID(refId) {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := &Session{}
|
||||
|
||||
// Build query.
|
||||
if err := UnscopedDb().Where("ref_id = ?", refId).First(m).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// RegenerateID regenerated the random session ID.
|
||||
func (m *Session) RegenerateID() *Session {
|
||||
if m.ID == "" {
|
||||
|
|
22
internal/form/search_sessions.go
Normal file
22
internal/form/search_sessions.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package form
|
||||
|
||||
// SearchSessions represents a session search form.
|
||||
type SearchSessions struct {
|
||||
Query string `form:"q"`
|
||||
UID string `form:"uid"`
|
||||
Count int `form:"count" binding:"required" serialize:"-"`
|
||||
Offset int `form:"offset" serialize:"-"`
|
||||
Order string `form:"order" serialize:"-"`
|
||||
}
|
||||
|
||||
func (f *SearchSessions) GetQuery() string {
|
||||
return f.Query
|
||||
}
|
||||
|
||||
func (f *SearchSessions) SetQuery(q string) {
|
||||
f.Query = q
|
||||
}
|
||||
|
||||
func (f *SearchSessions) ParseQueryString() error {
|
||||
return ParseQueryString(f)
|
||||
}
|
|
@ -22,7 +22,3 @@ func (f *SearchUsers) SetQuery(q string) {
|
|||
func (f *SearchUsers) ParseQueryString() error {
|
||||
return ParseQueryString(f)
|
||||
}
|
||||
|
||||
func NewSearchUsers(query string) SearchUsers {
|
||||
return SearchUsers{Query: query}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ func Sessions(limit, offset int, sortOrder, search string) (result entity.Sessio
|
|||
} else if rnd.IsUID(search, entity.UserUID) {
|
||||
stmt = stmt.Where("user_uid = ?", search)
|
||||
} else if search != "" {
|
||||
stmt = stmt.Where("user_name LIKE ?", search+"%")
|
||||
stmt = stmt.Where("user_name LIKE ? OR auth_provider LIKE ?", search+"%", search+"%")
|
||||
}
|
||||
|
||||
if sortOrder == "" {
|
||||
|
|
45
internal/search/sessions.go
Normal file
45
internal/search/sessions.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// Sessions finds user sessions.
|
||||
func Sessions(f form.SearchSessions) (result entity.Sessions, err error) {
|
||||
result = entity.Sessions{}
|
||||
stmt := Db()
|
||||
|
||||
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 != "" {
|
||||
stmt = stmt.Where("user_name LIKE ? OR auth_provider LIKE ?", search+"%", search+"%")
|
||||
}
|
||||
|
||||
if sortOrder == "" {
|
||||
sortOrder = "last_active, user_name"
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
stmt = stmt.Limit(limit)
|
||||
|
||||
if offset > 0 {
|
||||
stmt = stmt.Offset(offset)
|
||||
}
|
||||
}
|
||||
|
||||
err = stmt.Order(sortOrder).Find(&result).Error
|
||||
|
||||
return result, err
|
||||
}
|
49
internal/search/sessions_test.go
Normal file
49
internal/search/sessions_test.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
||||
func TestSessions(t *testing.T) {
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
if results, err := Sessions(form.SearchSessions{}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.LessOrEqual(t, 2, len(results))
|
||||
//t.Logf("sessions: %#v", results)
|
||||
}
|
||||
})
|
||||
t.Run("Limit", func(t *testing.T) {
|
||||
if results, err := Sessions(form.SearchSessions{Count: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.LessOrEqual(t, 1, len(results))
|
||||
//t.Logf("sessions: %#v", results)
|
||||
}
|
||||
})
|
||||
t.Run("Offset", func(t *testing.T) {
|
||||
if results, err := Sessions(form.SearchSessions{Offset: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.LessOrEqual(t, 2, len(results))
|
||||
//t.Logf("sessions: %#v", results)
|
||||
}
|
||||
})
|
||||
t.Run("SearchAlice", func(t *testing.T) {
|
||||
if results, err := Sessions(form.SearchSessions{Count: 100, Query: "alice"}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
t.Logf("sessions: %#v", results)
|
||||
assert.LessOrEqual(t, 1, len(results))
|
||||
if len(results) > 0 {
|
||||
assert.Equal(t, "69be27ac5ca305b394046a83f6fda18167ca3d3f2dbe7ac0", results[0].ID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", results[0].UserUID)
|
||||
assert.Equal(t, "alice", results[0].UserName)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
49
internal/search/users_test.go
Normal file
49
internal/search/users_test.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package search
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
)
|
||||
|
||||
func TestUsers(t *testing.T) {
|
||||
t.Run("Default", func(t *testing.T) {
|
||||
if results, err := Users(form.SearchUsers{}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.LessOrEqual(t, 2, len(results))
|
||||
//t.Logf("sessions: %#v", results)
|
||||
}
|
||||
})
|
||||
t.Run("Limit", func(t *testing.T) {
|
||||
if results, err := Users(form.SearchUsers{Count: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.LessOrEqual(t, 1, len(results))
|
||||
//t.Logf("sessions: %#v", results)
|
||||
}
|
||||
})
|
||||
t.Run("Offset", func(t *testing.T) {
|
||||
if results, err := Users(form.SearchUsers{Offset: 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
assert.LessOrEqual(t, 2, len(results))
|
||||
//t.Logf("sessions: %#v", results)
|
||||
}
|
||||
})
|
||||
t.Run("SearchAlice", func(t *testing.T) {
|
||||
if results, err := Users(form.SearchUsers{Count: 100, Query: "alice"}); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
t.Logf("users: %#v", results)
|
||||
assert.LessOrEqual(t, 1, len(results))
|
||||
if len(results) > 0 {
|
||||
assert.Equal(t, 5, results[0].ID)
|
||||
assert.Equal(t, "uqxetse3cy5eo9z2", results[0].UserUID)
|
||||
assert.Equal(t, "alice", results[0].UserName)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue