2FA: Add two-factor authentication key model and tests #782 #808 #3943

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2024-01-19 13:45:30 +01:00
parent 57d95b5a3c
commit 4ba32a7220
17 changed files with 475 additions and 162 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -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:"-"`

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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