d0ad3c23fb
Signed-off-by: Michael Mayer <michael@photoprism.app>
590 lines
14 KiB
Go
590 lines
14 KiB
Go
package entity
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/dustin/go-humanize/english"
|
|
"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"
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
|
"github.com/photoprism/photoprism/pkg/report"
|
|
"github.com/photoprism/photoprism/pkg/rnd"
|
|
"github.com/photoprism/photoprism/pkg/unix"
|
|
)
|
|
|
|
// ClientUID is the unique ID prefix.
|
|
const (
|
|
ClientUID = byte('c')
|
|
)
|
|
|
|
// Clients represents a list of client applications.
|
|
type Clients []Client
|
|
|
|
// Client represents a client application.
|
|
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:200;index;" json:"UserName" yaml:"UserName,omitempty"`
|
|
user *User `gorm:"-" yaml:"-"`
|
|
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"`
|
|
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"AuthProvider" yaml:"AuthProvider,omitempty"`
|
|
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"AuthMethod" yaml:"AuthMethod,omitempty"`
|
|
AuthScope string `gorm:"size:1024;default:'';" json:"AuthScope" yaml:"AuthScope,omitempty"`
|
|
AuthExpires int64 `json:"AuthExpires" yaml:"AuthExpires,omitempty"`
|
|
AuthTokens int64 `json:"AuthTokens" yaml:"AuthTokens,omitempty"`
|
|
AuthEnabled bool `json:"AuthEnabled" yaml:"AuthEnabled,omitempty"`
|
|
LastActive int64 `json:"LastActive" yaml:"LastActive,omitempty"`
|
|
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
|
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
|
}
|
|
|
|
// TableName returns the entity table name.
|
|
func (Client) TableName() string {
|
|
return "auth_clients"
|
|
}
|
|
|
|
// NewClient returns a new client application instance.
|
|
func NewClient() *Client {
|
|
return &Client{
|
|
UserUID: "",
|
|
ClientName: "",
|
|
ClientRole: acl.RoleClient.String(),
|
|
ClientType: authn.ClientConfidential,
|
|
ClientURL: "",
|
|
CallbackURL: "",
|
|
AuthProvider: authn.ProviderClientCredentials.String(),
|
|
AuthMethod: authn.MethodOAuth2.String(),
|
|
AuthScope: "",
|
|
AuthExpires: unix.Hour,
|
|
AuthTokens: 5,
|
|
AuthEnabled: true,
|
|
LastActive: 0,
|
|
}
|
|
}
|
|
|
|
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
|
|
func (m *Client) BeforeCreate(scope *gorm.Scope) error {
|
|
if rnd.IsUID(m.ClientUID, ClientUID) {
|
|
return nil
|
|
}
|
|
|
|
m.ClientUID = rnd.GenerateUID(ClientUID)
|
|
|
|
return scope.SetColumn("ClientUID", m.ClientUID)
|
|
}
|
|
|
|
// FindClientByUID returns the matching client or nil if it was not found.
|
|
func FindClientByUID(uid string) *Client {
|
|
if rnd.InvalidUID(uid, ClientUID) {
|
|
return nil
|
|
}
|
|
|
|
m := &Client{}
|
|
|
|
// Find matching record.
|
|
if err := UnscopedDb().First(m, "client_uid = ?", uid).Error; err != nil {
|
|
return nil
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// UID returns the client uid string.
|
|
func (m *Client) UID() string {
|
|
return m.ClientUID
|
|
}
|
|
|
|
// HasUID tests if the client has a valid uid.
|
|
func (m *Client) HasUID() bool {
|
|
return rnd.IsUID(m.ClientUID, ClientUID)
|
|
}
|
|
|
|
// NoUID tests if the client does not have a valid uid.
|
|
func (m *Client) NoUID() bool {
|
|
return !m.HasUID()
|
|
}
|
|
|
|
// Name returns the client name string.
|
|
func (m *Client) Name() string {
|
|
return m.ClientName
|
|
}
|
|
|
|
// HasName tests if the client has a name.
|
|
func (m *Client) HasName() bool {
|
|
return m.ClientName != ""
|
|
}
|
|
|
|
// NoName tests if the client does not have a name.
|
|
func (m *Client) NoName() bool {
|
|
return !m.HasName()
|
|
}
|
|
|
|
// String returns the client id or name for use in logs and reports.
|
|
func (m *Client) String() string {
|
|
if m == nil {
|
|
return report.NotAssigned
|
|
} else if m.HasUID() {
|
|
return m.UID()
|
|
} else if m.HasName() {
|
|
return m.Name()
|
|
}
|
|
|
|
return report.NotAssigned
|
|
}
|
|
|
|
// SetName sets a custom client name.
|
|
func (m *Client) SetName(s string) *Client {
|
|
if s = clean.Name(s); s != "" {
|
|
m.ClientName = s
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// SetRole sets the client role specified as string.
|
|
func (m *Client) SetRole(role string) *Client {
|
|
if role != "" {
|
|
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 user who owns the client, if any.
|
|
func (m *Client) User() *User {
|
|
if m.user != nil {
|
|
return m.user
|
|
} else if m.UserUID == "" {
|
|
return &User{}
|
|
}
|
|
|
|
if u := FindUserByUID(m.UserUID); u != nil {
|
|
m.user = u
|
|
return m.user
|
|
}
|
|
|
|
return &User{}
|
|
}
|
|
|
|
// HasUser checks the client belongs to a user.
|
|
func (m *Client) HasUser() bool {
|
|
return rnd.IsUID(m.UserUID, UserUID)
|
|
}
|
|
|
|
// SetUser sets the user to which the client belongs.
|
|
func (m *Client) SetUser(u *User) *Client {
|
|
if u == nil {
|
|
return m
|
|
}
|
|
|
|
// Update user references.
|
|
m.user = u
|
|
m.UserUID = u.UserUID
|
|
m.UserName = u.UserName
|
|
|
|
return m
|
|
}
|
|
|
|
// UserInfo reports the user that is assigned to this client.
|
|
func (m *Client) UserInfo() string {
|
|
if m == nil {
|
|
return ""
|
|
} else if m.UserUID == "" {
|
|
return report.NotAssigned
|
|
} else if m.UserName != "" {
|
|
return m.UserName
|
|
}
|
|
|
|
return m.UserUID
|
|
}
|
|
|
|
// AuthInfo reports the authentication configured for this client.
|
|
func (m *Client) AuthInfo() string {
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
|
|
provider := m.Provider()
|
|
method := m.Method()
|
|
|
|
if method.IsDefault() {
|
|
return provider.Pretty()
|
|
}
|
|
|
|
if provider.IsDefault() {
|
|
return method.Pretty()
|
|
}
|
|
|
|
return fmt.Sprintf("%s (%s)", provider.Pretty(), method.Pretty())
|
|
}
|
|
|
|
// Create new entity in the database.
|
|
func (m *Client) Create() error {
|
|
return Db().Create(m).Error
|
|
}
|
|
|
|
// Save updates the record in the database or inserts a new record if it does not already exist.
|
|
func (m *Client) Save() error {
|
|
if err := Db().Save(m).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete related sessions if authentication is disabled.
|
|
if m.AuthEnabled {
|
|
return nil
|
|
} else if _, err := m.DeleteSessions(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete marks the entity as deleted.
|
|
func (m *Client) Delete() (err error) {
|
|
if m.ClientUID == "" {
|
|
return fmt.Errorf("client uid is missing")
|
|
}
|
|
|
|
if _, err = m.DeleteSessions(); err != nil {
|
|
return err
|
|
}
|
|
|
|
err = Db().Delete(m).Error
|
|
|
|
return err
|
|
}
|
|
|
|
// DeleteSessions deletes all sessions that belong to this client.
|
|
func (m *Client) DeleteSessions() (deleted int, err error) {
|
|
if m.ClientUID == "" {
|
|
return 0, fmt.Errorf("client uid is missing")
|
|
}
|
|
|
|
if deleted = DeleteClientSessions(m, "", 0); deleted > 0 {
|
|
event.AuditInfo([]string{"client %s", "deleted %s"}, m.String(), english.Plural(deleted, "session", "sessions"))
|
|
}
|
|
|
|
return deleted, nil
|
|
}
|
|
|
|
// Deleted checks if the client has been deleted.
|
|
func (m *Client) Deleted() bool {
|
|
if m == nil {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Disabled checks if the client authentication has been disabled.
|
|
func (m *Client) Disabled() bool {
|
|
if m == nil {
|
|
return true
|
|
}
|
|
|
|
return !m.AuthEnabled
|
|
}
|
|
|
|
// Updates multiple properties in the database.
|
|
func (m *Client) Updates(values interface{}) error {
|
|
return UnscopedDb().Model(m).Updates(values).Error
|
|
}
|
|
|
|
// NewSecret sets a random client secret and returns it if successful.
|
|
func (m *Client) NewSecret() (secret string, err error) {
|
|
if !m.HasUID() {
|
|
return "", fmt.Errorf("invalid client uid")
|
|
}
|
|
|
|
secret = rnd.ClientSecret()
|
|
|
|
if err = m.SetSecret(secret); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return secret, nil
|
|
}
|
|
|
|
// SetSecret updates the current client secret or returns an error otherwise.
|
|
func (m *Client) SetSecret(secret string) (err error) {
|
|
if !m.HasUID() {
|
|
return fmt.Errorf("invalid client uid")
|
|
} else if !rnd.IsClientSecret(secret) {
|
|
return fmt.Errorf("invalid client secret")
|
|
}
|
|
|
|
pw := NewPassword(m.ClientUID, secret, false)
|
|
|
|
if err = pw.Save(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// HasSecret checks if the given client secret is correct.
|
|
func (m *Client) HasSecret(s string) bool {
|
|
return !m.WrongSecret(s)
|
|
}
|
|
|
|
// WrongSecret checks if the given client secret is incorrect.
|
|
func (m *Client) WrongSecret(s string) bool {
|
|
if !m.HasUID() {
|
|
return true
|
|
}
|
|
|
|
// Empty secret?
|
|
if s == "" {
|
|
return true
|
|
}
|
|
|
|
// Fetch secret.
|
|
pw := FindPassword(m.ClientUID)
|
|
|
|
// Found?
|
|
if pw == nil {
|
|
return true
|
|
}
|
|
|
|
// Invalid?
|
|
if pw.IsWrong(s) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// Provider returns the client authentication provider.
|
|
func (m *Client) Provider() authn.ProviderType {
|
|
return authn.Provider(m.AuthProvider)
|
|
}
|
|
|
|
// SetProvider sets a custom client authentication provider.
|
|
func (m *Client) SetProvider(provider authn.ProviderType) *Client {
|
|
if !provider.IsDefault() {
|
|
m.AuthProvider = provider.String()
|
|
}
|
|
return m
|
|
}
|
|
|
|
// Method returns the client authentication method.
|
|
func (m *Client) Method() authn.MethodType {
|
|
return authn.Method(m.AuthMethod)
|
|
}
|
|
|
|
// SetMethod sets a custom client authentication method.
|
|
func (m *Client) SetMethod(method authn.MethodType) *Client {
|
|
if !method.IsDefault() {
|
|
m.AuthMethod = method.String()
|
|
}
|
|
return m
|
|
}
|
|
|
|
// Scope returns the client authorization scope.
|
|
func (m *Client) Scope() string {
|
|
return clean.Scope(m.AuthScope)
|
|
}
|
|
|
|
// SetScope sets a custom client authorization scope.
|
|
func (m *Client) SetScope(s string) *Client {
|
|
if s = clean.Scope(s); s != "" {
|
|
m.AuthScope = clean.Scope(s)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// UpdateLastActive sets the last activity of the client to now.
|
|
func (m *Client) UpdateLastActive() *Client {
|
|
if !m.HasUID() {
|
|
return m
|
|
}
|
|
|
|
m.LastActive = unix.Time()
|
|
|
|
if err := Db().Model(m).UpdateColumn("LastActive", m.LastActive).Error; err != nil {
|
|
log.Debugf("client: failed to update %s timestamp (%s)", m.ClientUID, err)
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// NewSession creates a new client session.
|
|
func (m *Client) NewSession(c *gin.Context) *Session {
|
|
// Create, initialize, and return new session.
|
|
return NewSession(m.AuthExpires, 0).SetContext(c).SetClient(m)
|
|
}
|
|
|
|
// EnforceAuthTokenLimit deletes client sessions above the configured limit and returns the number of deleted sessions.
|
|
func (m *Client) EnforceAuthTokenLimit() (deleted int) {
|
|
if m == nil {
|
|
return 0
|
|
} else if !m.HasUID() {
|
|
return 0
|
|
} else if m.AuthTokens < 0 {
|
|
return 0
|
|
}
|
|
|
|
return DeleteClientSessions(m, authn.MethodOAuth2, m.AuthTokens)
|
|
}
|
|
|
|
// Expires returns the auth expiration duration.
|
|
func (m *Client) Expires() time.Duration {
|
|
return time.Duration(m.AuthExpires) * time.Second
|
|
}
|
|
|
|
// SetExpires sets a custom auth expiration time in seconds.
|
|
func (m *Client) SetExpires(i int64) *Client {
|
|
if i != 0 {
|
|
m.AuthExpires = i
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// Tokens returns maximum number of access tokens this client can create.
|
|
func (m *Client) Tokens() int64 {
|
|
if m.AuthTokens == 0 {
|
|
return 1
|
|
}
|
|
|
|
return m.AuthTokens
|
|
}
|
|
|
|
// SetTokens sets a custom access token limit for this client.
|
|
func (m *Client) SetTokens(i int64) *Client {
|
|
if i != 0 {
|
|
m.AuthTokens = i
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// Report returns the entity values as rows.
|
|
func (m *Client) Report(skipEmpty bool) (rows [][]string, cols []string) {
|
|
cols = []string{"Name", "Value"}
|
|
|
|
// Extract model values.
|
|
values, _, err := ModelValues(m, "ClientUID")
|
|
|
|
// Ok?
|
|
if err != nil {
|
|
return rows, cols
|
|
}
|
|
|
|
rows = make([][]string, 0, len(values))
|
|
|
|
for k, v := range values {
|
|
s := fmt.Sprintf("%#v", v)
|
|
|
|
// Skip empty values?
|
|
if !skipEmpty || s != "" {
|
|
rows = append(rows, []string{k, s})
|
|
}
|
|
}
|
|
|
|
return rows, cols
|
|
}
|
|
|
|
// SetFormValues sets the values specified in the form.
|
|
func (m *Client) SetFormValues(frm form.Client) *Client {
|
|
if frm.UserUID == "" && frm.UserName == "" {
|
|
// Client does not belong to a specific user or the user remains unchanged.
|
|
} else if u := FindUser(User{UserUID: frm.UserUID, UserName: frm.UserName}); u != nil {
|
|
m.SetUser(u)
|
|
}
|
|
|
|
// Set custom client UID?
|
|
if id := frm.ID(); m.ClientUID == "" && id != "" {
|
|
m.ClientUID = id
|
|
}
|
|
|
|
// Set values from form.
|
|
m.SetName(frm.Name())
|
|
m.SetProvider(frm.Provider())
|
|
m.SetMethod(frm.Method())
|
|
m.SetScope(frm.Scope())
|
|
m.SetTokens(frm.Tokens())
|
|
m.SetExpires(frm.Expires())
|
|
|
|
// Enable authentication?
|
|
if frm.AuthEnabled {
|
|
m.AuthEnabled = true
|
|
}
|
|
|
|
// Replace empty values with defaults.
|
|
if m.AuthProvider == "" {
|
|
m.AuthProvider = authn.ProviderClientCredentials.String()
|
|
}
|
|
|
|
if m.AuthMethod == "" {
|
|
m.AuthMethod = authn.MethodOAuth2.String()
|
|
}
|
|
|
|
if m.AuthScope == "" {
|
|
m.AuthScope = "*"
|
|
}
|
|
|
|
if m.AuthExpires <= 0 {
|
|
m.AuthExpires = unix.Hour
|
|
}
|
|
|
|
if m.AuthTokens <= 0 {
|
|
m.AuthTokens = -1
|
|
}
|
|
|
|
return m
|
|
}
|
|
|
|
// Validate checks the client application properties before saving them.
|
|
func (m *Client) Validate() (err error) {
|
|
// Empty client name?
|
|
if m.ClientName == "" {
|
|
return errors.New("client name must not be empty")
|
|
}
|
|
|
|
// Empty client type?
|
|
if m.ClientType == "" {
|
|
return errors.New("client type must not be empty")
|
|
}
|
|
|
|
// Empty authorization method?
|
|
if m.AuthMethod == "" {
|
|
return errors.New("authorization method must not be empty")
|
|
}
|
|
|
|
// Empty authorization scope?
|
|
if m.AuthScope == "" {
|
|
return errors.New("authorization scope must not be empty")
|
|
}
|
|
|
|
return nil
|
|
}
|