Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
57d95b5a3c
commit
4ba32a7220
17 changed files with 475 additions and 162 deletions
|
@ -105,7 +105,7 @@ func clientsAddAction(ctx *cli.Context) error {
|
|||
client.UID(),
|
||||
client.Name(),
|
||||
client.UserInfo(),
|
||||
client.Method().String(),
|
||||
client.AuthInfo(),
|
||||
client.AclRole().String(),
|
||||
client.Scope(),
|
||||
report.Bool(client.AuthEnabled, report.Yes, report.No),
|
||||
|
|
|
@ -59,7 +59,7 @@ func clientsListAction(ctx *cli.Context) error {
|
|||
client.UID(),
|
||||
client.Name(),
|
||||
client.UserInfo(),
|
||||
client.Method().String(),
|
||||
client.AuthInfo(),
|
||||
client.AclRole().String(),
|
||||
client.Scope(),
|
||||
report.Bool(client.AuthEnabled, report.Yes, report.No),
|
||||
|
|
|
@ -26,24 +26,25 @@ 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:"-"`
|
||||
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"`
|
||||
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"` // TODO: Enforce limit for number of tokens.
|
||||
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:"-"`
|
||||
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
|
||||
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:"-"`
|
||||
DeletedAt *time.Time `sql:"index" json:"DeletedAt,omitempty" yaml:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the entity table name.
|
||||
|
@ -54,18 +55,19 @@ func (Client) TableName() string {
|
|||
// NewClient returns a new client application instance.
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
UserUID: "",
|
||||
ClientName: "",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "",
|
||||
AuthExpires: UnixHour,
|
||||
AuthTokens: 5,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
UserUID: "",
|
||||
ClientName: "",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "",
|
||||
AuthExpires: UnixHour,
|
||||
AuthTokens: 5,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -166,9 +168,11 @@ func (m *Client) SetUser(u *User) *Client {
|
|||
return m
|
||||
}
|
||||
|
||||
// UserInfo returns user identification info.
|
||||
// UserInfo reports the user that is assigned to this client.
|
||||
func (m *Client) UserInfo() string {
|
||||
if m.UserUID == "" {
|
||||
if m == nil {
|
||||
return ""
|
||||
} else if m.UserUID == "" {
|
||||
return ""
|
||||
} else if m.UserName != "" {
|
||||
return m.UserName
|
||||
|
@ -177,6 +181,26 @@ func (m *Client) UserInfo() string {
|
|||
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
|
||||
|
@ -267,6 +291,11 @@ func (m *Client) WrongSecret(s string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// Provider returns the client authentication provider.
|
||||
func (m *Client) Provider() authn.ProviderType {
|
||||
return authn.Provider(m.AuthProvider)
|
||||
}
|
||||
|
||||
// Method returns the client authentication method.
|
||||
func (m *Client) Method() authn.MethodType {
|
||||
return authn.Method(m.AuthMethod)
|
||||
|
|
|
@ -25,90 +25,95 @@ func (m ClientMap) Pointer(name string) *Client {
|
|||
|
||||
var ClientFixtures = ClientMap{
|
||||
"alice": {
|
||||
ClientUID: "cs5gfen1bgxz7s9i",
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
ClientName: "Alice",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "*",
|
||||
AuthExpires: UnixDay,
|
||||
AuthTokens: -1,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
ClientUID: "cs5gfen1bgxz7s9i",
|
||||
UserUID: UserFixtures.Pointer("alice").UserUID,
|
||||
UserName: UserFixtures.Pointer("alice").UserName,
|
||||
user: UserFixtures.Pointer("alice"),
|
||||
ClientName: "Alice",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "*",
|
||||
AuthExpires: UnixDay,
|
||||
AuthTokens: -1,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
},
|
||||
"bob": {
|
||||
ClientUID: "cs5gfsvbd7ejzn8m",
|
||||
UserUID: UserFixtures.Pointer("bob").UserUID,
|
||||
UserName: UserFixtures.Pointer("bob").UserName,
|
||||
user: UserFixtures.Pointer("bob"),
|
||||
ClientName: "Bob",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientPublic,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "*",
|
||||
AuthExpires: 0,
|
||||
AuthTokens: -1,
|
||||
AuthEnabled: false,
|
||||
LastActive: 0,
|
||||
ClientUID: "cs5gfsvbd7ejzn8m",
|
||||
UserUID: UserFixtures.Pointer("bob").UserUID,
|
||||
UserName: UserFixtures.Pointer("bob").UserName,
|
||||
user: UserFixtures.Pointer("bob"),
|
||||
ClientName: "Bob",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientPublic,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "*",
|
||||
AuthExpires: 0,
|
||||
AuthTokens: -1,
|
||||
AuthEnabled: false,
|
||||
LastActive: 0,
|
||||
},
|
||||
"metrics": {
|
||||
ClientUID: "cs5cpu17n6gj2qo5",
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
user: nil,
|
||||
ClientName: "Monitoring",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "metrics",
|
||||
AuthExpires: UnixHour,
|
||||
AuthTokens: 2,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
ClientUID: "cs5cpu17n6gj2qo5",
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
user: nil,
|
||||
ClientName: "Monitoring",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "metrics",
|
||||
AuthExpires: UnixHour,
|
||||
AuthTokens: 2,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
},
|
||||
"Unknown": {
|
||||
ClientUID: "cs5cpu17n6gj2jh6",
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
user: nil,
|
||||
ClientName: "Unknown",
|
||||
ClientRole: acl.RoleNone.String(),
|
||||
ClientType: authn.ClientUnknown,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthMethod: authn.MethodUnknown.String(),
|
||||
AuthScope: "*",
|
||||
AuthExpires: UnixHour,
|
||||
AuthTokens: 2,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
ClientUID: "cs5cpu17n6gj2jh6",
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
user: nil,
|
||||
ClientName: "Unknown",
|
||||
ClientRole: acl.RoleNone.String(),
|
||||
ClientType: authn.ClientUnknown,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodUnknown.String(),
|
||||
AuthScope: "*",
|
||||
AuthExpires: UnixHour,
|
||||
AuthTokens: 2,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
},
|
||||
"deleted": {
|
||||
ClientUID: "cs5cpu17n6gj2gf7",
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
user: nil,
|
||||
ClientName: "Deleted Monitoring",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "metrics",
|
||||
AuthExpires: UnixHour,
|
||||
AuthTokens: 2,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
DeletedAt: TimePointer(),
|
||||
ClientUID: "cs5cpu17n6gj2gf7",
|
||||
UserUID: "",
|
||||
UserName: "",
|
||||
user: nil,
|
||||
ClientName: "Deleted Monitoring",
|
||||
ClientRole: acl.RoleClient.String(),
|
||||
ClientType: authn.ClientConfidential,
|
||||
ClientURL: "",
|
||||
CallbackURL: "",
|
||||
AuthProvider: authn.ProviderClientCredentials.String(),
|
||||
AuthMethod: authn.MethodOAuth2.String(),
|
||||
AuthScope: "metrics",
|
||||
AuthExpires: UnixHour,
|
||||
AuthTokens: 2,
|
||||
AuthEnabled: true,
|
||||
LastActive: 0,
|
||||
DeletedAt: TimePointer(),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -244,10 +244,33 @@ func TestClient_NewSecret(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestClient_Method(t *testing.T) {
|
||||
func TestClient_Provider(t *testing.T) {
|
||||
t.Run("New", func(t *testing.T) {
|
||||
client := NewClient()
|
||||
assert.Equal(t, authn.ProviderClientCredentials, client.Provider())
|
||||
})
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
alice := ClientFixtures.Get("alice")
|
||||
assert.Equal(t, alice.Method(), authn.MethodOAuth2)
|
||||
client := ClientFixtures.Get("alice")
|
||||
assert.Equal(t, authn.ProviderClientCredentials, client.Provider())
|
||||
})
|
||||
t.Run("Bob", func(t *testing.T) {
|
||||
client := ClientFixtures.Get("bob")
|
||||
assert.Equal(t, authn.ProviderClientCredentials, client.Provider())
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_Method(t *testing.T) {
|
||||
t.Run("New", func(t *testing.T) {
|
||||
client := NewClient()
|
||||
assert.Equal(t, authn.MethodOAuth2, client.Method())
|
||||
})
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
client := ClientFixtures.Get("alice")
|
||||
assert.Equal(t, authn.MethodOAuth2, client.Method())
|
||||
})
|
||||
t.Run("Bob", func(t *testing.T) {
|
||||
client := ClientFixtures.Get("bob")
|
||||
assert.Equal(t, authn.MethodOAuth2, client.Method())
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -355,6 +378,30 @@ func TestClient_Expires(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestClient_UserInfo(t *testing.T) {
|
||||
t.Run("New", func(t *testing.T) {
|
||||
assert.Equal(t, "", NewClient().UserInfo())
|
||||
})
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
assert.Equal(t, "alice", ClientFixtures.Pointer("alice").UserInfo())
|
||||
})
|
||||
t.Run("Metrics", func(t *testing.T) {
|
||||
assert.Equal(t, "", ClientFixtures.Pointer("metrics").UserInfo())
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_AuthInfo(t *testing.T) {
|
||||
t.Run("New", func(t *testing.T) {
|
||||
assert.Equal(t, "Client Credentials (OAuth2)", NewClient().AuthInfo())
|
||||
})
|
||||
t.Run("Alice", func(t *testing.T) {
|
||||
assert.Equal(t, "Client Credentials (OAuth2)", ClientFixtures.Pointer("alice").AuthInfo())
|
||||
})
|
||||
t.Run("Metrics", func(t *testing.T) {
|
||||
assert.Equal(t, "Client Credentials (OAuth2)", ClientFixtures.Pointer("metrics").AuthInfo())
|
||||
})
|
||||
}
|
||||
|
||||
func TestClient_Report(t *testing.T) {
|
||||
t.Run("Metrics", func(t *testing.T) {
|
||||
m := ClientFixtures.Get("metrics")
|
||||
|
|
121
internal/entity/auth_key.go
Normal file
121
internal/entity/auth_key.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
)
|
||||
|
||||
// AuthKey represents a two-factor authentication key.
|
||||
type AuthKey struct {
|
||||
UID string `gorm:"type:VARBINARY(255);primary_key;" json:"UID"`
|
||||
KeyType string `gorm:"size:64;default:'';primary_key;" json:"KeyType" yaml:"KeyType"`
|
||||
KeyURL string `gorm:"size:2048;default:'';column:key_url;" json:"-" yaml:"-"`
|
||||
key *otp.Key `gorm:"-" yaml:"-"`
|
||||
RecoveryCodes string `gorm:"size:2048;default:'';" json:"-" yaml:"-"`
|
||||
RecoveryEmail string `gorm:"size:255;" json:"RecoveryEmail" yaml:"RecoveryEmail,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
||||
}
|
||||
|
||||
// TableName returns the entity table name.
|
||||
func (AuthKey) TableName() string {
|
||||
return "auth_keys"
|
||||
}
|
||||
|
||||
// NewAuthKey returns a new two-factor authentication key or nil if no valid entity UID was provided.
|
||||
func NewAuthKey(uid string, keyUrl string) (*AuthKey, error) {
|
||||
// Create new authentication key.
|
||||
m := &AuthKey{
|
||||
UID: uid,
|
||||
KeyURL: keyUrl,
|
||||
RecoveryCodes: "",
|
||||
RecoveryEmail: "",
|
||||
}
|
||||
|
||||
// Return an error if the uid or key are invalid.
|
||||
if rnd.InvalidUID(uid, 0) {
|
||||
return m, errors.New("auth: invalid uid")
|
||||
} else if keyUrl == "" {
|
||||
return m, errors.New("auth: invalid url")
|
||||
} else if err := m.SetKeyURL(keyUrl); err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// SetUID assigns a valid entity UID.
|
||||
func (m *AuthKey) SetUID(uid string) *AuthKey {
|
||||
if rnd.IsUID(uid, 0) {
|
||||
m.UID = uid
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// InvalidUID checks if the entity UID is invalid.
|
||||
func (m *AuthKey) InvalidUID() bool {
|
||||
if m == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return !rnd.IsUID(m.UID, 0)
|
||||
}
|
||||
|
||||
// Key returns the parsed two-factor authentication key or nil if the KeyURL is invalid.
|
||||
func (m *AuthKey) Key() *otp.Key {
|
||||
if m == nil {
|
||||
return nil
|
||||
} else if m.key != nil {
|
||||
return m.key
|
||||
}
|
||||
|
||||
key, err := otp.NewKeyFromURL(m.KeyURL)
|
||||
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.key = key
|
||||
|
||||
return m.key
|
||||
}
|
||||
|
||||
// SetKey sets a new two-factor authentication key.
|
||||
func (m *AuthKey) SetKey(key *otp.Key) error {
|
||||
if key == nil {
|
||||
return errors.New("auth: key is nil")
|
||||
}
|
||||
|
||||
if keyType := key.Type(); authn.MethodTOTP.NotEqual(keyType) {
|
||||
return fmt.Errorf("auth: invalid key type %s", clean.Log(keyType))
|
||||
} else if key.Secret() == "" {
|
||||
return errors.New("auth: invalid key secret")
|
||||
}
|
||||
|
||||
m.KeyType = key.Type()
|
||||
m.KeyURL = key.URL()
|
||||
m.key = key
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetKeyURL sets a new two-factor authentication key based on the URL provided.
|
||||
func (m *AuthKey) SetKeyURL(keyUrl string) error {
|
||||
key, err := otp.NewKeyFromURL(keyUrl)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("auth: %s", err)
|
||||
} else if key == nil {
|
||||
return errors.New("auth: failed to parse url")
|
||||
}
|
||||
|
||||
return m.SetKey(key)
|
||||
}
|
97
internal/entity/auth_key_test.go
Normal file
97
internal/entity/auth_key_test.go
Normal file
|
@ -0,0 +1,97 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/authn"
|
||||
)
|
||||
|
||||
func TestNewAuthKey(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
uid := "us7gqkzx1g9a82h4"
|
||||
keyUrl := "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=sha256&digits=8"
|
||||
|
||||
m, err := NewAuthKey(uid, keyUrl)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("NewAuthKey/Valid: %#v", m)
|
||||
|
||||
assert.NotNil(t, m)
|
||||
assert.Equal(t, uid, m.UID)
|
||||
assert.Equal(t, authn.MethodTOTP.String(), m.KeyType)
|
||||
assert.True(t, authn.MethodTOTP.Equal(m.KeyType))
|
||||
assert.Equal(t, "", m.RecoveryCodes)
|
||||
assert.Equal(t, "", m.RecoveryEmail)
|
||||
})
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
m, err := NewAuthKey("foo", "")
|
||||
|
||||
t.Logf("TestNewAuthKey/Invalid: %#v", m)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.NotNil(t, m)
|
||||
assert.Equal(t, "foo", m.UID)
|
||||
assert.True(t, authn.MethodTOTP.NotEqual(m.KeyType))
|
||||
assert.Equal(t, "", m.RecoveryCodes)
|
||||
assert.Equal(t, "", m.RecoveryEmail)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthKey_Key(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
uid := "us7gqkzx1g9a82h4"
|
||||
keyUrl := "otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=sha256&digits=8"
|
||||
|
||||
m, err := NewAuthKey(uid, keyUrl)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
key := m.Key()
|
||||
|
||||
t.Logf("TestAuthKey_Key/Valid: %#v", m)
|
||||
|
||||
assert.NotNil(t, m)
|
||||
assert.Equal(t, authn.MethodTOTP.String(), key.Type())
|
||||
assert.Equal(t, keyUrl, key.URL())
|
||||
assert.Equal(t, uint64(30), key.Period())
|
||||
assert.Equal(t, "JBSWY3DPEHPK3PXP", key.Secret())
|
||||
assert.Equal(t, keyUrl, key.String())
|
||||
assert.Equal(t, "alice@google.com", key.AccountName())
|
||||
assert.Equal(t, "SHA256", key.Algorithm().String())
|
||||
assert.Equal(t, otp.Digits(8), key.Digits())
|
||||
assert.Equal(t, 8, key.Digits().Length())
|
||||
|
||||
})
|
||||
t.Run("Invalid", func(t *testing.T) {
|
||||
m, err := NewAuthKey("foo", "")
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("error expected")
|
||||
}
|
||||
|
||||
key := m.Key()
|
||||
|
||||
t.Logf("TestAuthKey_Key/Invalid: %#v", m)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.NotNil(t, m)
|
||||
assert.Equal(t, "", key.Type())
|
||||
assert.Equal(t, "", key.URL())
|
||||
assert.Equal(t, uint64(30), key.Period())
|
||||
assert.Equal(t, "", key.Secret())
|
||||
assert.Equal(t, "", key.String())
|
||||
assert.Equal(t, "", key.AccountName())
|
||||
assert.Equal(t, "SHA1", key.Algorithm().String())
|
||||
assert.Equal(t, otp.DigitsSix, key.Digits())
|
||||
assert.Equal(t, 6, key.Digits().Length())
|
||||
})
|
||||
|
||||
}
|
|
@ -33,14 +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"`
|
||||
authToken string `gorm:"-"`
|
||||
authToken string `gorm:"-" yaml:"-"`
|
||||
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:"-"`
|
||||
user *User `gorm:"-" yaml:"-"`
|
||||
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:"-"`
|
||||
client *Client `gorm:"-" yaml:"-"`
|
||||
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"`
|
||||
|
@ -56,7 +56,7 @@ type Session struct {
|
|||
IdToken string `gorm:"type:VARBINARY(1024);column:id_token;default:'';" json:"IdToken,omitempty" yaml:"IdToken,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:"-"`
|
||||
data *SessionData `gorm:"-" 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:"-"`
|
||||
|
|
|
@ -47,6 +47,7 @@ type User struct {
|
|||
UUID string `gorm:"type:VARBINARY(64);column:user_uuid;index;" json:"UUID,omitempty" yaml:"UUID,omitempty"`
|
||||
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"`
|
||||
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"AuthMethod" yaml:"AuthMethod,omitempty"`
|
||||
AuthID string `gorm:"type:VARBINARY(255);index;default:'';" json:"AuthID" yaml:"AuthID,omitempty"`
|
||||
UserName string `gorm:"size:200;index;" json:"Name" yaml:"Name,omitempty"`
|
||||
DisplayName string `gorm:"size:200;" json:"DisplayName" yaml:"DisplayName,omitempty"`
|
||||
|
|
|
@ -18,6 +18,7 @@ var Entities = Tables{
|
|||
migrate.Version{}.TableName(): &migrate.Version{},
|
||||
Error{}.TableName(): &Error{},
|
||||
Password{}.TableName(): &Password{},
|
||||
AuthKey{}.TableName(): &AuthKey{},
|
||||
User{}.TableName(): &User{},
|
||||
UserDetails{}.TableName(): &UserDetails{},
|
||||
UserSettings{}.TableName(): &UserSettings{},
|
||||
|
|
|
@ -29,7 +29,7 @@ type Face struct {
|
|||
Collisions int `json:"Collisions" yaml:"Collisions,omitempty"`
|
||||
CollisionRadius float64 `json:"CollisionRadius" yaml:"CollisionRadius,omitempty"`
|
||||
EmbeddingJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"-" yaml:"EmbeddingJSON,omitempty"`
|
||||
embedding face.Embedding `gorm:"-"`
|
||||
embedding face.Embedding `gorm:"-" yaml:"-"`
|
||||
MatchedAt *time.Time `json:"MatchedAt" yaml:"MatchedAt,omitempty"`
|
||||
CreatedAt time.Time `json:"CreatedAt" yaml:"CreatedAt,omitempty"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt" yaml:"UpdatedAt,omitempty"`
|
||||
|
|
|
@ -39,7 +39,7 @@ type Marker struct {
|
|||
FaceDist float64 `gorm:"default:-1;" json:"FaceDist" yaml:"FaceDist,omitempty"`
|
||||
face *Face `gorm:"foreignkey:FaceID;association_foreignkey:ID;association_autoupdate:false;association_autocreate:false;association_save_reference:false"`
|
||||
EmbeddingsJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"-" yaml:"EmbeddingsJSON,omitempty"`
|
||||
embeddings face.Embeddings `gorm:"-"`
|
||||
embeddings face.Embeddings `gorm:"-" yaml:"-"`
|
||||
LandmarksJSON json.RawMessage `gorm:"type:MEDIUMBLOB;" json:"-" yaml:"LandmarksJSON,omitempty"`
|
||||
X float32 `gorm:"type:FLOAT;" json:"X" yaml:"X,omitempty"`
|
||||
Y float32 `gorm:"type:FLOAT;" json:"Y" yaml:"Y,omitempty"`
|
||||
|
|
|
@ -33,7 +33,7 @@ func TestNewClientFromCli(t *testing.T) {
|
|||
c := cli.NewContext(app, globalSet, nil)
|
||||
|
||||
client := NewClientFromCli(c)
|
||||
assert.Equal(t, authn.Method2FA, client.Method())
|
||||
assert.Equal(t, authn.MethodTOTP, client.Method())
|
||||
assert.Equal(t, "webdav", client.Scope())
|
||||
assert.Equal(t, "Test", client.Name())
|
||||
})
|
||||
|
|
|
@ -17,7 +17,7 @@ const (
|
|||
MethodAccessToken MethodType = "access_token"
|
||||
MethodOAuth2 MethodType = "oauth2"
|
||||
MethodOIDC MethodType = "oidc"
|
||||
Method2FA MethodType = "2fa"
|
||||
MethodTOTP MethodType = "totp"
|
||||
MethodUnknown MethodType = ""
|
||||
)
|
||||
|
||||
|
@ -35,8 +35,8 @@ func (t MethodType) String() string {
|
|||
return string(MethodOAuth2)
|
||||
case "openid":
|
||||
return string(MethodOIDC)
|
||||
case "totp":
|
||||
return string(Method2FA)
|
||||
case "2fa", "otp":
|
||||
return string(MethodTOTP)
|
||||
default:
|
||||
return string(t)
|
||||
}
|
||||
|
@ -61,8 +61,8 @@ func (t MethodType) Pretty() string {
|
|||
return "OAuth2"
|
||||
case MethodOIDC:
|
||||
return "OIDC"
|
||||
case Method2FA:
|
||||
return "2FA"
|
||||
case MethodTOTP:
|
||||
return "TOTP/2FA"
|
||||
default:
|
||||
return txt.UpperFirst(t.String())
|
||||
}
|
||||
|
@ -77,8 +77,8 @@ func Method(s string) MethodType {
|
|||
return MethodOAuth2
|
||||
case "sso":
|
||||
return MethodOIDC
|
||||
case "two-factor", "totp":
|
||||
return Method2FA
|
||||
case "TOTP/2FA", "2FA", "2fa", "OTP", "otp":
|
||||
return MethodTOTP
|
||||
default:
|
||||
return MethodType(clean.TypeLower(s))
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ func TestMethodType_String(t *testing.T) {
|
|||
assert.Equal(t, "access_token", MethodAccessToken.String())
|
||||
assert.Equal(t, "oauth2", MethodOAuth2.String())
|
||||
assert.Equal(t, "oidc", MethodOIDC.String())
|
||||
assert.Equal(t, "2fa", Method2FA.String())
|
||||
assert.Equal(t, "totp", MethodTOTP.String())
|
||||
assert.Equal(t, "default", MethodUnknown.String())
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ func TestMethodType_IsDefault(t *testing.T) {
|
|||
assert.Equal(t, false, MethodAccessToken.IsDefault())
|
||||
assert.Equal(t, false, MethodOAuth2.IsDefault())
|
||||
assert.Equal(t, false, MethodOIDC.IsDefault())
|
||||
assert.Equal(t, false, Method2FA.IsDefault())
|
||||
assert.Equal(t, false, MethodTOTP.IsDefault())
|
||||
assert.Equal(t, true, MethodUnknown.IsDefault())
|
||||
}
|
||||
|
||||
|
@ -29,17 +29,19 @@ func TestMethodType_Pretty(t *testing.T) {
|
|||
assert.Equal(t, "Access Token", MethodAccessToken.Pretty())
|
||||
assert.Equal(t, "OAuth2", MethodOAuth2.Pretty())
|
||||
assert.Equal(t, "OIDC", MethodOIDC.Pretty())
|
||||
assert.Equal(t, "2FA", Method2FA.Pretty())
|
||||
assert.Equal(t, "TOTP/2FA", MethodTOTP.Pretty())
|
||||
assert.Equal(t, "Default", MethodUnknown.Pretty())
|
||||
}
|
||||
|
||||
func TestMethodType_Equal(t *testing.T) {
|
||||
assert.True(t, Method2FA.Equal("2fa"))
|
||||
assert.True(t, MethodTOTP.Equal("totp"))
|
||||
assert.False(t, MethodTOTP.Equal("2fa"))
|
||||
assert.False(t, MethodAccessToken.Equal("2fa"))
|
||||
}
|
||||
|
||||
func TestMethodType_NotEqual(t *testing.T) {
|
||||
assert.False(t, Method2FA.NotEqual("2fa"))
|
||||
assert.True(t, MethodTOTP.NotEqual("2fa"))
|
||||
assert.False(t, MethodTOTP.NotEqual("totp"))
|
||||
assert.True(t, MethodAccessToken.NotEqual("2fa"))
|
||||
}
|
||||
|
||||
|
@ -50,6 +52,7 @@ func TestMethod(t *testing.T) {
|
|||
assert.Equal(t, MethodOAuth2, Method("oauth2"))
|
||||
assert.Equal(t, MethodOIDC, Method("oidc"))
|
||||
assert.Equal(t, MethodOIDC, Method("sso"))
|
||||
assert.Equal(t, Method2FA, Method("2fa"))
|
||||
assert.Equal(t, Method2FA, Method("totp"))
|
||||
assert.Equal(t, MethodTOTP, Method("2fa"))
|
||||
assert.Equal(t, MethodTOTP, Method("totp"))
|
||||
assert.Equal(t, MethodTOTP, Method("TOTP/2FA"))
|
||||
}
|
||||
|
|
|
@ -11,15 +11,16 @@ import (
|
|||
// ProviderType represents an authentication provider type.
|
||||
type ProviderType string
|
||||
|
||||
// Authentication providers.
|
||||
// Standard authentication provider types.
|
||||
const (
|
||||
ProviderDefault ProviderType = "default"
|
||||
ProviderClient ProviderType = "client"
|
||||
ProviderLocal ProviderType = "local"
|
||||
ProviderLDAP ProviderType = "ldap"
|
||||
ProviderLink ProviderType = "link"
|
||||
ProviderNone ProviderType = "none"
|
||||
ProviderUnknown ProviderType = ""
|
||||
ProviderDefault ProviderType = "default"
|
||||
ProviderClient ProviderType = "client"
|
||||
ProviderClientCredentials ProviderType = "client_credentials"
|
||||
ProviderLocal ProviderType = "local"
|
||||
ProviderLDAP ProviderType = "ldap"
|
||||
ProviderLink ProviderType = "link"
|
||||
ProviderNone ProviderType = "none"
|
||||
ProviderUnknown ProviderType = ""
|
||||
)
|
||||
|
||||
// RemoteProviders contains all remote auth providers.
|
||||
|
@ -35,6 +36,7 @@ var LocalProviders = list.List{
|
|||
// ClientProviders contains all client auth providers.
|
||||
var ClientProviders = list.List{
|
||||
string(ProviderClient),
|
||||
string(ProviderClientCredentials),
|
||||
}
|
||||
|
||||
// IsRemote checks if the provider is external.
|
||||
|
@ -66,6 +68,8 @@ func (t ProviderType) String() string {
|
|||
return string(ProviderLink)
|
||||
case "password":
|
||||
return string(ProviderLocal)
|
||||
case "oauth2", "client credentials":
|
||||
return string(ProviderClientCredentials)
|
||||
default:
|
||||
return string(t)
|
||||
}
|
||||
|
@ -88,6 +92,8 @@ func (t ProviderType) Pretty() string {
|
|||
return "LDAP/AD"
|
||||
case ProviderClient:
|
||||
return "Client"
|
||||
case ProviderClientCredentials:
|
||||
return "Client Credentials"
|
||||
default:
|
||||
return txt.UpperFirst(t.String())
|
||||
}
|
||||
|
@ -104,6 +110,8 @@ func Provider(s string) ProviderType {
|
|||
return ProviderLocal
|
||||
case "ldap", "ad", "ldap/ad", "ldap\\ad":
|
||||
return ProviderLDAP
|
||||
case "oauth2", "client credentials":
|
||||
return ProviderClientCredentials
|
||||
default:
|
||||
return ProviderType(clean.TypeLower(s))
|
||||
}
|
||||
|
|
|
@ -16,35 +16,36 @@ func TestProviderType_String(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestProviderType_IsRemote(t *testing.T) {
|
||||
assert.Equal(t, false, ProviderLocal.IsRemote())
|
||||
assert.Equal(t, true, ProviderLDAP.IsRemote())
|
||||
assert.Equal(t, false, ProviderNone.IsRemote())
|
||||
assert.Equal(t, false, ProviderDefault.IsRemote())
|
||||
assert.Equal(t, false, ProviderUnknown.IsRemote())
|
||||
assert.False(t, ProviderLocal.IsRemote())
|
||||
assert.True(t, ProviderLDAP.IsRemote())
|
||||
assert.False(t, ProviderNone.IsRemote())
|
||||
assert.False(t, ProviderDefault.IsRemote())
|
||||
assert.False(t, ProviderUnknown.IsRemote())
|
||||
}
|
||||
|
||||
func TestProviderType_IsLocal(t *testing.T) {
|
||||
assert.Equal(t, true, ProviderLocal.IsLocal())
|
||||
assert.Equal(t, false, ProviderLDAP.IsLocal())
|
||||
assert.Equal(t, false, ProviderNone.IsLocal())
|
||||
assert.Equal(t, false, ProviderDefault.IsLocal())
|
||||
assert.Equal(t, false, ProviderUnknown.IsLocal())
|
||||
assert.True(t, ProviderLocal.IsLocal())
|
||||
assert.False(t, ProviderLDAP.IsLocal())
|
||||
assert.False(t, ProviderNone.IsLocal())
|
||||
assert.False(t, ProviderDefault.IsLocal())
|
||||
assert.False(t, ProviderUnknown.IsLocal())
|
||||
}
|
||||
|
||||
func TestProviderType_IsDefault(t *testing.T) {
|
||||
assert.Equal(t, false, ProviderLocal.IsDefault())
|
||||
assert.Equal(t, false, ProviderLDAP.IsDefault())
|
||||
assert.Equal(t, false, ProviderNone.IsDefault())
|
||||
assert.Equal(t, true, ProviderDefault.IsDefault())
|
||||
assert.Equal(t, true, ProviderUnknown.IsDefault())
|
||||
assert.False(t, ProviderLocal.IsDefault())
|
||||
assert.False(t, ProviderLDAP.IsDefault())
|
||||
assert.False(t, ProviderNone.IsDefault())
|
||||
assert.True(t, ProviderDefault.IsDefault())
|
||||
assert.True(t, ProviderUnknown.IsDefault())
|
||||
}
|
||||
|
||||
func TestProviderType_IsClient(t *testing.T) {
|
||||
assert.Equal(t, false, ProviderLocal.IsClient())
|
||||
assert.Equal(t, false, ProviderLDAP.IsClient())
|
||||
assert.Equal(t, false, ProviderNone.IsClient())
|
||||
assert.Equal(t, false, ProviderDefault.IsClient())
|
||||
assert.Equal(t, true, ProviderClient.IsClient())
|
||||
assert.False(t, ProviderLocal.IsClient())
|
||||
assert.False(t, ProviderLDAP.IsClient())
|
||||
assert.False(t, ProviderNone.IsClient())
|
||||
assert.False(t, ProviderDefault.IsClient())
|
||||
assert.True(t, ProviderClient.IsClient())
|
||||
assert.True(t, ProviderClientCredentials.IsClient())
|
||||
}
|
||||
|
||||
func TestProviderType_Equal(t *testing.T) {
|
||||
|
|
Loading…
Reference in a new issue