Skip to content

Commit 2a99c97

Browse files
authored
feat(ldap): support webdav, ftp and sftp login (#1746)
* feat(ldap): support webdav, ftp and sftp login * fix: apply suggestions of Copilot * feat(ldap) support ftp, sftp and webdav auto-register
1 parent 0a407c3 commit 2a99c97

File tree

9 files changed

+268
-133
lines changed

9 files changed

+268
-133
lines changed

internal/bootstrap/data/setting.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ func InitialSettings() []model.SettingItem {
208208
// ldap settings
209209
{Key: conf.LdapLoginEnabled, Value: "false", Type: conf.TypeBool, Group: model.LDAP, Flag: model.PUBLIC},
210210
{Key: conf.LdapServer, Value: "", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},
211+
{Key: conf.LdapSkipTlsVerify, Value: "false", Type: conf.TypeBool, Group: model.LDAP, Flag: model.PRIVATE},
211212
{Key: conf.LdapManagerDN, Value: "", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},
212213
{Key: conf.LdapManagerPassword, Value: "", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},
213214
{Key: conf.LdapUserSearchBase, Value: "", Type: conf.TypeString, Group: model.LDAP, Flag: model.PRIVATE},

internal/conf/const.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ const (
115115
// ldap
116116
LdapLoginEnabled = "ldap_login_enabled"
117117
LdapServer = "ldap_server"
118+
LdapSkipTlsVerify = "ldap_skip_tls_verify"
118119
LdapManagerDN = "ldap_manager_dn"
119120
LdapManagerPassword = "ldap_manager_password"
120121
LdapUserSearchBase = "ldap_user_search_base"

internal/model/user.go

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type User struct {
5959
OtpSecret string `json:"-"`
6060
SsoID string `json:"sso_id"` // unique by sso platform
6161
Authn string `gorm:"type:text" json:"-"`
62+
AllowLdap bool `json:"allow_ldap" gorm:"default:true"`
6263
}
6364

6465
func (u *User) IsGuest() bool {
@@ -90,64 +91,124 @@ func (u *User) SetPassword(pwd string) *User {
9091
return u
9192
}
9293

94+
func CanSeeHides(permission int32) bool {
95+
return permission&1 == 1
96+
}
97+
9398
func (u *User) CanSeeHides() bool {
94-
return u.Permission&1 == 1
99+
return CanSeeHides(u.Permission)
100+
}
101+
102+
func CanAccessWithoutPassword(permission int32) bool {
103+
return (permission>>1)&1 == 1
95104
}
96105

97106
func (u *User) CanAccessWithoutPassword() bool {
98-
return (u.Permission>>1)&1 == 1
107+
return CanAccessWithoutPassword(u.Permission)
108+
}
109+
110+
func CanAddOfflineDownloadTasks(permission int32) bool {
111+
return (permission>>2)&1 == 1
99112
}
100113

101114
func (u *User) CanAddOfflineDownloadTasks() bool {
102-
return (u.Permission>>2)&1 == 1
115+
return CanAddOfflineDownloadTasks(u.Permission)
116+
}
117+
118+
func CanWrite(permission int32) bool {
119+
return (permission>>3)&1 == 1
103120
}
104121

105122
func (u *User) CanWrite() bool {
106-
return (u.Permission>>3)&1 == 1
123+
return CanWrite(u.Permission)
124+
}
125+
126+
func CanRename(permission int32) bool {
127+
return (permission>>4)&1 == 1
107128
}
108129

109130
func (u *User) CanRename() bool {
110-
return (u.Permission>>4)&1 == 1
131+
return CanRename(u.Permission)
132+
}
133+
134+
func CanMove(permission int32) bool {
135+
return (permission>>5)&1 == 1
111136
}
112137

113138
func (u *User) CanMove() bool {
114-
return (u.Permission>>5)&1 == 1
139+
return CanMove(u.Permission)
140+
}
141+
142+
func CanCopy(permission int32) bool {
143+
return (permission>>6)&1 == 1
115144
}
116145

117146
func (u *User) CanCopy() bool {
118-
return (u.Permission>>6)&1 == 1
147+
return CanCopy(u.Permission)
148+
}
149+
150+
func CanRemove(permission int32) bool {
151+
return (permission>>7)&1 == 1
119152
}
120153

121154
func (u *User) CanRemove() bool {
122-
return (u.Permission>>7)&1 == 1
155+
return CanRemove(u.Permission)
156+
}
157+
158+
func CanWebdavRead(permission int32) bool {
159+
return (permission>>8)&1 == 1
123160
}
124161

125162
func (u *User) CanWebdavRead() bool {
126-
return (u.Permission>>8)&1 == 1
163+
return CanWebdavRead(u.Permission)
164+
}
165+
166+
func CanWebdavManage(permission int32) bool {
167+
return (permission>>9)&1 == 1
127168
}
128169

129170
func (u *User) CanWebdavManage() bool {
130-
return (u.Permission>>9)&1 == 1
171+
return CanWebdavManage(u.Permission)
172+
}
173+
174+
func CanFTPAccess(permission int32) bool {
175+
return (permission>>10)&1 == 1
131176
}
132177

133178
func (u *User) CanFTPAccess() bool {
134-
return (u.Permission>>10)&1 == 1
179+
return CanFTPAccess(u.Permission)
180+
}
181+
182+
func CanFTPManage(permission int32) bool {
183+
return (permission>>11)&1 == 1
135184
}
136185

137186
func (u *User) CanFTPManage() bool {
138-
return (u.Permission>>11)&1 == 1
187+
return CanFTPManage(u.Permission)
188+
}
189+
190+
func CanReadArchives(permission int32) bool {
191+
return (permission>>12)&1 == 1
139192
}
140193

141194
func (u *User) CanReadArchives() bool {
142-
return (u.Permission>>12)&1 == 1
195+
return CanReadArchives(u.Permission)
196+
}
197+
198+
func CanDecompress(permission int32) bool {
199+
return (permission>>13)&1 == 1
143200
}
144201

145202
func (u *User) CanDecompress() bool {
146-
return (u.Permission>>13)&1 == 1
203+
return CanDecompress(u.Permission)
204+
}
205+
206+
func CanShare(permission int32) bool {
207+
return (permission>>14)&1 == 1
147208
}
148209

149210
func (u *User) CanShare() bool {
150-
return (u.Permission>>14)&1 == 1
211+
return CanShare(u.Permission)
151212
}
152213

153214
func (u *User) JoinPath(reqPath string) (string, error) {

server/common/ldap.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package common
2+
3+
import (
4+
"crypto/tls"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/OpenListTeam/OpenList/v4/internal/conf"
9+
"github.com/OpenListTeam/OpenList/v4/internal/model"
10+
"github.com/OpenListTeam/OpenList/v4/internal/op"
11+
"github.com/OpenListTeam/OpenList/v4/internal/setting"
12+
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
13+
"github.com/OpenListTeam/OpenList/v4/pkg/utils/random"
14+
"github.com/pkg/errors"
15+
log "github.com/sirupsen/logrus"
16+
"gopkg.in/ldap.v3"
17+
)
18+
19+
var ErrFailedLdapAuth = errors.New("failed to auth")
20+
21+
func HandleLdapLogin(username, password string) error {
22+
// Auth start
23+
ldapServer := setting.GetStr(conf.LdapServer)
24+
skipTlsVerify := setting.GetBool(conf.LdapSkipTlsVerify)
25+
ldapManagerDN := setting.GetStr(conf.LdapManagerDN)
26+
ldapManagerPassword := setting.GetStr(conf.LdapManagerPassword)
27+
ldapUserSearchBase := setting.GetStr(conf.LdapUserSearchBase)
28+
ldapUserSearchFilter := setting.GetStr(conf.LdapUserSearchFilter) // (uid=%s)
29+
30+
// Connect to LdapServer
31+
l, err := dial(ldapServer, skipTlsVerify)
32+
if err != nil {
33+
return errors.WithMessagef(err, "failed to connect to LDAP")
34+
}
35+
defer l.Close()
36+
37+
// First bind with a read only user
38+
if ldapManagerDN != "" && ldapManagerPassword != "" {
39+
err = l.Bind(ldapManagerDN, ldapManagerPassword)
40+
if err != nil {
41+
return errors.WithMessagef(err, "failed to bind to LDAP")
42+
}
43+
}
44+
45+
// Search for the given username
46+
searchRequest := ldap.NewSearchRequest(
47+
ldapUserSearchBase,
48+
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
49+
fmt.Sprintf(ldapUserSearchFilter, ldap.EscapeFilter(username)),
50+
[]string{"dn"},
51+
nil,
52+
)
53+
sr, err := l.Search(searchRequest)
54+
if err != nil {
55+
return errors.WithMessagef(err, "failed login ldap: LDAP search failed")
56+
}
57+
if len(sr.Entries) != 1 {
58+
return errors.New("failed login ldap: user does not exist or too many entries returned")
59+
}
60+
userDN := sr.Entries[0].DN
61+
62+
// Bind as the user to verify their password
63+
err = l.Bind(userDN, password)
64+
if err != nil {
65+
return errors.WithMessagef(ErrFailedLdapAuth, "%v", err)
66+
}
67+
log.Infof("LDAP auth successful for %s", username)
68+
// Auth finished
69+
return nil
70+
}
71+
72+
func LdapRegister(username string) (*model.User, error) {
73+
if username == "" {
74+
return nil, errors.New("cannot get username from ldap provider")
75+
}
76+
user := &model.User{
77+
Username: username,
78+
Password: "",
79+
Authn: "[]",
80+
Permission: int32(setting.GetInt(conf.LdapDefaultPermission, 0)),
81+
BasePath: setting.GetStr(conf.LdapDefaultDir),
82+
Role: 0,
83+
Disabled: false,
84+
AllowLdap: true,
85+
}
86+
user.SetPassword(random.String(16))
87+
if err := op.CreateUser(user); err != nil {
88+
return nil, err
89+
}
90+
return user, nil
91+
}
92+
93+
func dial(ldapServer string, skipTlsVerify ...bool) (*ldap.Conn, error) {
94+
tlsEnabled := false
95+
if strings.HasPrefix(ldapServer, "ldaps://") {
96+
tlsEnabled = true
97+
ldapServer = strings.TrimPrefix(ldapServer, "ldaps://")
98+
} else if strings.HasPrefix(ldapServer, "ldap://") {
99+
ldapServer = strings.TrimPrefix(ldapServer, "ldap://")
100+
}
101+
102+
if tlsEnabled {
103+
return ldap.DialTLS("tcp", ldapServer, &tls.Config{InsecureSkipVerify: utils.IsBool(skipTlsVerify...)})
104+
} else {
105+
return ldap.Dial("tcp", ldapServer)
106+
}
107+
}

server/ftp.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/OpenListTeam/OpenList/v4/internal/op"
2020
"github.com/OpenListTeam/OpenList/v4/internal/setting"
2121
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
22+
"github.com/OpenListTeam/OpenList/v4/server/common"
2223
"github.com/OpenListTeam/OpenList/v4/server/ftp"
2324
ftpserver "github.com/fclairamb/ftpserverlib"
2425
)
@@ -112,6 +113,12 @@ func (d *FtpMainDriver) ClientDisconnected(cc ftpserver.ClientContext) {
112113
}
113114

114115
func (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string) (ftpserver.ClientDriver, error) {
116+
ip := cc.RemoteAddr().String()
117+
count, ok := model.LoginCache.Get(ip)
118+
if ok && count >= model.DefaultMaxAuthRetries {
119+
model.LoginCache.Expire(ip, model.DefaultLockDuration)
120+
return nil, errors.New("Too many unsuccessful sign-in attempts have been made using an incorrect username or password, Try again later.")
121+
}
115122
var userObj *model.User
116123
var err error
117124
if user == "anonymous" || user == "guest" {
@@ -121,17 +128,24 @@ func (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string)
121128
}
122129
} else {
123130
userObj, err = op.GetUserByName(user)
124-
if err != nil {
125-
return nil, err
131+
if err == nil {
132+
err = userObj.ValidateRawPassword(pass)
133+
if err != nil && setting.GetBool(conf.LdapLoginEnabled) && userObj.AllowLdap {
134+
err = common.HandleLdapLogin(user, pass)
135+
}
136+
} else if setting.GetBool(conf.LdapLoginEnabled) && model.CanFTPAccess(int32(setting.GetInt(conf.LdapDefaultPermission, 0))) {
137+
userObj, err = tryLdapLoginAndRegister(user, pass)
126138
}
127-
passHash := model.StaticHash(pass)
128-
if err = userObj.ValidatePwdStaticHash(passHash); err != nil {
139+
if err != nil {
140+
model.LoginCache.Set(ip, count+1)
129141
return nil, err
130142
}
131143
}
132144
if userObj.Disabled || !userObj.CanFTPAccess() {
145+
model.LoginCache.Set(ip, count+1)
133146
return nil, errors.New("user is not allowed to access via FTP")
134147
}
148+
model.LoginCache.Del(ip)
135149

136150
ctx := context.Background()
137151
ctx = context.WithValue(ctx, conf.UserKey, userObj)
@@ -140,7 +154,7 @@ func (d *FtpMainDriver) AuthUser(cc ftpserver.ClientContext, user, pass string)
140154
} else {
141155
ctx = context.WithValue(ctx, conf.MetaPassKey, "")
142156
}
143-
ctx = context.WithValue(ctx, conf.ClientIPKey, cc.RemoteAddr().String())
157+
ctx = context.WithValue(ctx, conf.ClientIPKey, ip)
144158
ctx = context.WithValue(ctx, conf.ProxyHeaderKey, d.proxyHeader)
145159
return ftp.NewAferoAdapter(ctx), nil
146160
}

0 commit comments

Comments
 (0)