Auth: Refactor user roles and auth providers in entity model #98

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-03-13 16:04:37 +01:00
parent 3465d0e348
commit 5b73101442
11 changed files with 84 additions and 13 deletions

View file

@ -97,6 +97,25 @@ debug = true
action = "search" action = "search"
object = "*" object = "*"
[[users]]
name = "contributor"
givenname = "Contributor"
objectClass = "user"
displayName = "Contributor"
sn = "Contributor"
userPrincipalName = "contributor@example.com"
mail = "contributor@example.com"
uidnumber = 5009
primarygroup = 5509
loginShell = "/bin/bash"
otherGroups = [5508]
passsha256 = "4314c1fe282face45336b1422a3285c5ff31a39c8e24425615fa53a43b718493" # photoprism
[[users.customattributes]]
photoprismUploadPath = ["contrib"]
[[users.capabilities]]
action = "search"
object = "*"
[[users]] [[users]]
name = "mail" name = "mail"
objectClass = "user" objectClass = "user"
@ -144,3 +163,7 @@ debug = true
[[groups]] [[groups]]
name = "PhotoPrism-webdav" name = "PhotoPrism-webdav"
gidnumber = 5508 gidnumber = 5508
[[groups]]
name = "PhotoPrism-contributor"
gidnumber = 5509

View file

@ -1,6 +1,7 @@
package entity package entity
import ( import (
"fmt"
"net/http" "net/http"
"time" "time"
@ -49,7 +50,15 @@ func AuthLocal(user *User, f form.Login, m *Session) (err error) {
} }
// Login allowed? // Login allowed?
if !user.CanLogIn() { if !user.Provider().IsDefault() && !user.Provider().IsLocal() {
message := fmt.Sprintf("%s authentication disabled", authn.ProviderLocal.String())
if m != nil {
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
event.LoginError(m.IP(), "api", name, m.UserAgent, message)
m.Status = http.StatusUnauthorized
}
return i18n.Error(i18n.ErrInvalidCredentials)
} else if !user.CanLogIn() {
message := "account disabled" message := "account disabled"
if m != nil { if m != nil {
event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name)) event.AuditWarn([]string{m.IP(), "session %s", "login as %s", message}, m.RefID, clean.LogQuote(name))
@ -102,7 +111,7 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
m.SetProvider(provider) m.SetProvider(provider)
} }
// Share token provided? // Link token provided?
if f.HasToken() { if f.HasToken() {
user = m.User() user = m.User()
@ -127,7 +136,7 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) {
return i18n.Error(i18n.ErrInvalidLink) return i18n.Error(i18n.ErrInvalidLink)
} else { } else {
m.SetData(data) m.SetData(data)
m.SetProvider(authn.ProviderToken) m.SetProvider(authn.ProviderLink)
event.AuditInfo([]string{m.IP(), "session %s", "token redeemed for %d shares"}, m.RefID, shares, data) event.AuditInfo([]string{m.IP(), "session %s", "token redeemed for %d shares"}, m.RefID, shares, data)
} }

View file

@ -491,7 +491,7 @@ func (m *User) Provider() authn.ProviderType {
if m.AuthProvider != "" { if m.AuthProvider != "" {
return authn.ProviderType(m.AuthProvider) return authn.ProviderType(m.AuthProvider)
} else if m.ID == Visitor.ID { } else if m.ID == Visitor.ID {
return authn.ProviderToken return authn.ProviderLink
} else if m.ID == 1 { } else if m.ID == 1 {
return authn.ProviderLocal return authn.ProviderLocal
} else if m.UserName != "" && m.ID > 0 { } else if m.UserName != "" && m.ID > 0 {
@ -595,6 +595,20 @@ func (m *User) FullName() string {
return clean.NameCapitalized(strings.ReplaceAll(m.Handle(), ".", " ")) return clean.NameCapitalized(strings.ReplaceAll(m.Handle(), ".", " "))
} }
// SetRole sets the user role specified as string.
func (m *User) SetRole(role string) *User {
role = clean.Role(role)
switch role {
case "", "0", "false", "nil", "null", "nan":
m.UserRole = acl.RoleUnknown.String()
default:
m.UserRole = acl.ValidRoles[role].String()
}
return m
}
// AclRole returns the user role for ACL permission checks. // AclRole returns the user role for ACL permission checks.
func (m *User) AclRole() acl.Role { func (m *User) AclRole() acl.Role {
role := clean.Role(m.UserRole) role := clean.Role(m.UserRole)
@ -842,7 +856,7 @@ func (m *User) SetFormValues(frm form.User) *User {
m.SuperAdmin = frm.SuperAdmin m.SuperAdmin = frm.SuperAdmin
m.CanLogin = frm.CanLogin m.CanLogin = frm.CanLogin
m.WebDAV = frm.WebDAV m.WebDAV = frm.WebDAV
m.UserRole = frm.Role() m.SetRole(frm.Role())
m.UserAttr = frm.Attr() m.UserAttr = frm.Attr()
m.SetBasePath(frm.BasePath) m.SetBasePath(frm.BasePath)
m.SetUploadPath(frm.UploadPath) m.SetUploadPath(frm.UploadPath)
@ -1011,7 +1025,7 @@ func (m *User) SaveForm(f form.User, updateRights bool) error {
// Update user rights only if explicitly requested. // Update user rights only if explicitly requested.
if updateRights { if updateRights {
m.UserRole = f.Role() m.SetRole(f.Role())
m.SuperAdmin = f.SuperAdmin m.SuperAdmin = f.SuperAdmin
m.CanLogin = f.CanLogin m.CanLogin = f.CanLogin
@ -1025,12 +1039,12 @@ func (m *User) SaveForm(f form.User, updateRights bool) error {
// Ensure super admins never have a non-admin role. // Ensure super admins never have a non-admin role.
if m.SuperAdmin { if m.SuperAdmin {
m.UserRole = acl.RoleAdmin.String() m.SetRole(acl.RoleAdmin.String())
} }
// Make sure that the initial admin user cannot lock itself out. // Make sure that the initial admin user cannot lock itself out.
if m.ID == Admin.ID && (m.AclRole() != acl.RoleAdmin || !m.SuperAdmin || !m.CanLogin) { if m.ID == Admin.ID && (m.AclRole() != acl.RoleAdmin || !m.SuperAdmin || !m.CanLogin) {
m.UserRole = acl.RoleAdmin.String() m.SetRole(acl.RoleAdmin.String())
m.SuperAdmin = true m.SuperAdmin = true
m.CanLogin = true m.CanLogin = true
} }

View file

@ -23,7 +23,7 @@ func (m *User) SetValuesFromCli(ctx *cli.Context) error {
// User role. // User role.
if ctx.IsSet("role") { if ctx.IsSet("role") {
m.UserRole = frm.Role() m.SetRole(frm.Role())
} }
// Super-admin status. // Super-admin status.

View file

@ -51,7 +51,7 @@ var Visitor = User{
ID: -2, ID: -2,
UserUID: "u000000000000002", UserUID: "u000000000000002",
UserName: "", UserName: "",
AuthProvider: authn.ProviderToken.String(), AuthProvider: authn.ProviderLink.String(),
UserRole: acl.RoleVisitor.String(), UserRole: acl.RoleVisitor.String(),
DisplayName: VisitorDisplayName, DisplayName: VisitorDisplayName,
CanLogin: false, CanLogin: false,

View file

@ -1001,7 +1001,7 @@ func TestUser_Username(t *testing.T) {
func TestUser_Provider(t *testing.T) { func TestUser_Provider(t *testing.T) {
t.Run("Visitor", func(t *testing.T) { t.Run("Visitor", func(t *testing.T) {
assert.Equal(t, authn.ProviderToken, Visitor.Provider()) assert.Equal(t, authn.ProviderLink, Visitor.Provider())
}) })
t.Run("UnknownUser", func(t *testing.T) { t.Run("UnknownUser", func(t *testing.T) {
assert.Equal(t, authn.ProviderNone, UnknownUser.Provider()) assert.Equal(t, authn.ProviderNone, UnknownUser.Provider())

View file

@ -159,4 +159,10 @@ var DialectMySQL = Migrations{
Stage: "main", Stage: "main",
Statements: []string{"UPDATE auth_users SET auth_provider = 'local' WHERE id = 1;", "UPDATE auth_users SET auth_provider = 'none' WHERE id = -1;", "UPDATE auth_users SET auth_provider = 'token' WHERE id = -2;", "UPDATE auth_users SET auth_provider = 'default' WHERE auth_provider = '' OR auth_provider = 'password' OR auth_provider IS NULL;"}, Statements: []string{"UPDATE auth_users SET auth_provider = 'local' WHERE id = 1;", "UPDATE auth_users SET auth_provider = 'none' WHERE id = -1;", "UPDATE auth_users SET auth_provider = 'token' WHERE id = -2;", "UPDATE auth_users SET auth_provider = 'default' WHERE auth_provider = '' OR auth_provider = 'password' OR auth_provider IS NULL;"},
}, },
{
ID: "20230313-000001",
Dialect: "mysql",
Stage: "main",
Statements: []string{"UPDATE auth_users SET user_role = 'contributor' WHERE user_role = 'uploader';", "UPDATE auth_sessions SET auth_provider = 'link' WHERE auth_provider = 'token';"},
},
} }

View file

@ -87,4 +87,10 @@ var DialectSQLite3 = Migrations{
Stage: "main", Stage: "main",
Statements: []string{"UPDATE auth_users SET auth_provider = 'local' WHERE id = 1;", "UPDATE auth_users SET auth_provider = 'none' WHERE id = -1;", "UPDATE auth_users SET auth_provider = 'token' WHERE id = -2;", "UPDATE auth_users SET auth_provider = 'default' WHERE auth_provider = '' OR auth_provider = 'password' OR auth_provider IS NULL;"}, Statements: []string{"UPDATE auth_users SET auth_provider = 'local' WHERE id = 1;", "UPDATE auth_users SET auth_provider = 'none' WHERE id = -1;", "UPDATE auth_users SET auth_provider = 'token' WHERE id = -2;", "UPDATE auth_users SET auth_provider = 'default' WHERE auth_provider = '' OR auth_provider = 'password' OR auth_provider IS NULL;"},
}, },
{
ID: "20230313-000001",
Dialect: "sqlite3",
Stage: "main",
Statements: []string{"UPDATE auth_users SET user_role = 'contributor' WHERE user_role = 'uploader';", "UPDATE auth_sessions SET auth_provider = 'link' WHERE auth_provider = 'token';"},
},
} }

View file

@ -0,0 +1,2 @@
UPDATE auth_users SET user_role = 'contributor' WHERE user_role = 'uploader';
UPDATE auth_sessions SET auth_provider = 'link' WHERE auth_provider = 'token';

View file

@ -0,0 +1,2 @@
UPDATE auth_users SET user_role = 'contributor' WHERE user_role = 'uploader';
UPDATE auth_sessions SET auth_provider = 'link' WHERE auth_provider = 'token';

View file

@ -14,7 +14,7 @@ const (
ProviderDefault ProviderType = "default" ProviderDefault ProviderType = "default"
ProviderLocal ProviderType = "local" ProviderLocal ProviderType = "local"
ProviderLDAP ProviderType = "ldap" ProviderLDAP ProviderType = "ldap"
ProviderToken ProviderType = "token" ProviderLink ProviderType = "link"
ProviderNone ProviderType = "none" ProviderNone ProviderType = "none"
ProviderUnknown ProviderType = "" ProviderUnknown ProviderType = ""
) )
@ -39,11 +39,18 @@ func (t ProviderType) IsLocal() bool {
return list.Contains(LocalProviders, string(t)) return list.Contains(LocalProviders, string(t))
} }
// IsDefault checks if this is the default provider.
func (t ProviderType) IsDefault() bool {
return t.String() == ProviderDefault.String()
}
// String returns the provider identifier as a string. // String returns the provider identifier as a string.
func (t ProviderType) String() string { func (t ProviderType) String() string {
switch t { switch t {
case "": case "":
return string(ProviderDefault) return string(ProviderDefault)
case "token":
return string(ProviderLink)
case "password": case "password":
return string(ProviderLocal) return string(ProviderLocal)
default: default:
@ -66,6 +73,8 @@ func Provider(s string) ProviderType {
switch s { switch s {
case "", "-", "null", "nil", "0", "false": case "", "-", "null", "nil", "0", "false":
return ProviderDefault return ProviderDefault
case "token", "url":
return ProviderLink
case "pass", "passwd", "password": case "pass", "passwd", "password":
return ProviderLocal return ProviderLocal
case "ldap", "ad", "ldap/ad", "ldap\\ad": case "ldap", "ad", "ldap/ad", "ldap\\ad":