diff --git a/.ldap.cfg b/.ldap.cfg index b05310819..375784e09 100644 --- a/.ldap.cfg +++ b/.ldap.cfg @@ -97,6 +97,25 @@ debug = true action = "search" 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]] name = "mail" objectClass = "user" @@ -143,4 +162,8 @@ debug = true [[groups]] name = "PhotoPrism-webdav" - gidnumber = 5508 \ No newline at end of file + gidnumber = 5508 + +[[groups]] + name = "PhotoPrism-contributor" + gidnumber = 5509 diff --git a/internal/entity/auth_session_login.go b/internal/entity/auth_session_login.go index 844109467..b8b25a87c 100644 --- a/internal/entity/auth_session_login.go +++ b/internal/entity/auth_session_login.go @@ -1,6 +1,7 @@ package entity import ( + "fmt" "net/http" "time" @@ -49,7 +50,15 @@ func AuthLocal(user *User, f form.Login, m *Session) (err error) { } // 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" if m != nil { 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) } - // Share token provided? + // Link token provided? if f.HasToken() { user = m.User() @@ -127,7 +136,7 @@ func (m *Session) LogIn(f form.Login, c *gin.Context) (err error) { return i18n.Error(i18n.ErrInvalidLink) } else { 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) } diff --git a/internal/entity/auth_user.go b/internal/entity/auth_user.go index 4c9ce8395..5eacd39e1 100644 --- a/internal/entity/auth_user.go +++ b/internal/entity/auth_user.go @@ -491,7 +491,7 @@ func (m *User) Provider() authn.ProviderType { if m.AuthProvider != "" { return authn.ProviderType(m.AuthProvider) } else if m.ID == Visitor.ID { - return authn.ProviderToken + return authn.ProviderLink } else if m.ID == 1 { return authn.ProviderLocal } else if m.UserName != "" && m.ID > 0 { @@ -595,6 +595,20 @@ func (m *User) FullName() string { 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. func (m *User) AclRole() acl.Role { role := clean.Role(m.UserRole) @@ -842,7 +856,7 @@ func (m *User) SetFormValues(frm form.User) *User { m.SuperAdmin = frm.SuperAdmin m.CanLogin = frm.CanLogin m.WebDAV = frm.WebDAV - m.UserRole = frm.Role() + m.SetRole(frm.Role()) m.UserAttr = frm.Attr() m.SetBasePath(frm.BasePath) 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. if updateRights { - m.UserRole = f.Role() + m.SetRole(f.Role()) m.SuperAdmin = f.SuperAdmin 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. if m.SuperAdmin { - m.UserRole = acl.RoleAdmin.String() + m.SetRole(acl.RoleAdmin.String()) } // Make sure that the initial admin user cannot lock itself out. 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.CanLogin = true } diff --git a/internal/entity/auth_user_cli.go b/internal/entity/auth_user_cli.go index 14c51327d..514aafd0f 100644 --- a/internal/entity/auth_user_cli.go +++ b/internal/entity/auth_user_cli.go @@ -23,7 +23,7 @@ func (m *User) SetValuesFromCli(ctx *cli.Context) error { // User role. if ctx.IsSet("role") { - m.UserRole = frm.Role() + m.SetRole(frm.Role()) } // Super-admin status. diff --git a/internal/entity/auth_user_default.go b/internal/entity/auth_user_default.go index ae37a4ab7..5402f317c 100644 --- a/internal/entity/auth_user_default.go +++ b/internal/entity/auth_user_default.go @@ -51,7 +51,7 @@ var Visitor = User{ ID: -2, UserUID: "u000000000000002", UserName: "", - AuthProvider: authn.ProviderToken.String(), + AuthProvider: authn.ProviderLink.String(), UserRole: acl.RoleVisitor.String(), DisplayName: VisitorDisplayName, CanLogin: false, diff --git a/internal/entity/auth_user_test.go b/internal/entity/auth_user_test.go index f3a16bea5..6c4d2e17c 100644 --- a/internal/entity/auth_user_test.go +++ b/internal/entity/auth_user_test.go @@ -1001,7 +1001,7 @@ func TestUser_Username(t *testing.T) { func TestUser_Provider(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) { assert.Equal(t, authn.ProviderNone, UnknownUser.Provider()) diff --git a/internal/migrate/dialect_mysql.go b/internal/migrate/dialect_mysql.go index 048fd1daa..b6cd17c43 100644 --- a/internal/migrate/dialect_mysql.go +++ b/internal/migrate/dialect_mysql.go @@ -159,4 +159,10 @@ var DialectMySQL = Migrations{ 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;"}, }, + { + 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';"}, + }, } diff --git a/internal/migrate/dialect_sqlite3.go b/internal/migrate/dialect_sqlite3.go index c3f397f86..e18800aac 100644 --- a/internal/migrate/dialect_sqlite3.go +++ b/internal/migrate/dialect_sqlite3.go @@ -87,4 +87,10 @@ var DialectSQLite3 = Migrations{ 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;"}, }, + { + 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';"}, + }, } diff --git a/internal/migrate/mysql/20230313-000001.sql b/internal/migrate/mysql/20230313-000001.sql new file mode 100644 index 000000000..45fa91840 --- /dev/null +++ b/internal/migrate/mysql/20230313-000001.sql @@ -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'; \ No newline at end of file diff --git a/internal/migrate/sqlite3/20230313-000001.sql b/internal/migrate/sqlite3/20230313-000001.sql new file mode 100644 index 000000000..45fa91840 --- /dev/null +++ b/internal/migrate/sqlite3/20230313-000001.sql @@ -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'; \ No newline at end of file diff --git a/pkg/authn/providers.go b/pkg/authn/providers.go index 6eff43b5b..ce9400c10 100644 --- a/pkg/authn/providers.go +++ b/pkg/authn/providers.go @@ -14,7 +14,7 @@ const ( ProviderDefault ProviderType = "default" ProviderLocal ProviderType = "local" ProviderLDAP ProviderType = "ldap" - ProviderToken ProviderType = "token" + ProviderLink ProviderType = "link" ProviderNone ProviderType = "none" ProviderUnknown ProviderType = "" ) @@ -39,11 +39,18 @@ func (t ProviderType) IsLocal() bool { 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. func (t ProviderType) String() string { switch t { case "": return string(ProviderDefault) + case "token": + return string(ProviderLink) case "password": return string(ProviderLocal) default: @@ -66,6 +73,8 @@ func Provider(s string) ProviderType { switch s { case "", "-", "null", "nil", "0", "false": return ProviderDefault + case "token", "url": + return ProviderLink case "pass", "passwd", "password": return ProviderLocal case "ldap", "ad", "ldap/ad", "ldap\\ad":