/
dataprovider.go
3145 lines (2952 loc) · 107 KB
/
dataprovider.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Package dataprovider provides data access.
// It abstracts different data providers and exposes a common API.
package dataprovider
import (
"bufio"
"bytes"
"context"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"crypto/subtle"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"hash"
"io"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/GehirnInc/crypt"
"github.com/GehirnInc/crypt/apr1_crypt"
"github.com/GehirnInc/crypt/md5_crypt"
"github.com/GehirnInc/crypt/sha512_crypt"
"github.com/alexedwards/argon2id"
"github.com/go-chi/render"
"github.com/rs/xid"
"github.com/sftpgo/sdk"
passwordvalidator "github.com/wagslane/go-password-validator"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/ssh"
"github.com/drakkan/sftpgo/v2/httpclient"
"github.com/drakkan/sftpgo/v2/kms"
"github.com/drakkan/sftpgo/v2/logger"
"github.com/drakkan/sftpgo/v2/metric"
"github.com/drakkan/sftpgo/v2/mfa"
"github.com/drakkan/sftpgo/v2/plugin"
"github.com/drakkan/sftpgo/v2/util"
"github.com/drakkan/sftpgo/v2/vfs"
)
const (
// SQLiteDataProviderName defines the name for SQLite database provider
SQLiteDataProviderName = "sqlite"
// PGSQLDataProviderName defines the name for PostgreSQL database provider
PGSQLDataProviderName = "postgresql"
// MySQLDataProviderName defines the name for MySQL database provider
MySQLDataProviderName = "mysql"
// BoltDataProviderName defines the name for bbolt key/value store provider
BoltDataProviderName = "bolt"
// MemoryDataProviderName defines the name for memory provider
MemoryDataProviderName = "memory"
// CockroachDataProviderName defines the for CockroachDB provider
CockroachDataProviderName = "cockroachdb"
// DumpVersion defines the version for the dump.
// For restore/load we support the current version and the previous one
DumpVersion = 10
argonPwdPrefix = "$argon2id$"
bcryptPwdPrefix = "$2a$"
pbkdf2SHA1Prefix = "$pbkdf2-sha1$"
pbkdf2SHA256Prefix = "$pbkdf2-sha256$"
pbkdf2SHA512Prefix = "$pbkdf2-sha512$"
pbkdf2SHA256B64SaltPrefix = "$pbkdf2-b64salt-sha256$"
md5cryptPwdPrefix = "$1$"
md5cryptApr1PwdPrefix = "$apr1$"
sha512cryptPwdPrefix = "$6$"
trackQuotaDisabledError = "please enable track_quota in your configuration to use this method"
operationAdd = "add"
operationUpdate = "update"
operationDelete = "delete"
sqlPrefixValidChars = "abcdefghijklmnopqrstuvwxyz_0123456789"
maxHookResponseSize = 1048576 // 1MB
)
// Supported algorithms for hashing passwords.
// These algorithms can be used when SFTPGo hashes a plain text password
const (
HashingAlgoBcrypt = "bcrypt"
HashingAlgoArgon2ID = "argon2id"
)
// ordering constants
const (
OrderASC = "ASC"
OrderDESC = "DESC"
)
const (
protocolSSH = "SSH"
protocolFTP = "FTP"
protocolWebDAV = "DAV"
protocolHTTP = "HTTP"
)
var (
// SupportedProviders defines the supported data providers
SupportedProviders = []string{SQLiteDataProviderName, PGSQLDataProviderName, MySQLDataProviderName,
BoltDataProviderName, MemoryDataProviderName, CockroachDataProviderName}
// ValidPerms defines all the valid permissions for a user
ValidPerms = []string{PermAny, PermListItems, PermDownload, PermUpload, PermOverwrite, PermCreateDirs, PermRename,
PermRenameFiles, PermRenameDirs, PermDelete, PermDeleteFiles, PermDeleteDirs, PermCreateSymlinks, PermChmod,
PermChown, PermChtimes}
// ValidLoginMethods defines all the valid login methods
ValidLoginMethods = []string{SSHLoginMethodPublicKey, LoginMethodPassword, SSHLoginMethodKeyboardInteractive,
SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt, LoginMethodTLSCertificate,
LoginMethodTLSCertificateAndPwd}
// SSHMultiStepsLoginMethods defines the supported Multi-Step Authentications
SSHMultiStepsLoginMethods = []string{SSHLoginMethodKeyAndPassword, SSHLoginMethodKeyAndKeyboardInt}
// ErrNoAuthTryed defines the error for connection closed before authentication
ErrNoAuthTryed = errors.New("no auth tryed")
// ErrNotImplemented defines the error for features not supported for a particular data provider
ErrNotImplemented = errors.New("feature not supported with the configured data provider")
// ValidProtocols defines all the valid protcols
ValidProtocols = []string{protocolSSH, protocolFTP, protocolWebDAV, protocolHTTP}
// MFAProtocols defines the supported protocols for multi-factor authentication
MFAProtocols = []string{protocolHTTP, protocolSSH, protocolFTP}
// ErrNoInitRequired defines the error returned by InitProvider if no inizialization/update is required
ErrNoInitRequired = errors.New("the data provider is up to date")
// ErrInvalidCredentials defines the error to return if the supplied credentials are invalid
ErrInvalidCredentials = errors.New("invalid credentials")
// ErrLoginNotAllowedFromIP defines the error to return if login is denied from the current IP
ErrLoginNotAllowedFromIP = errors.New("login is not allowed from this IP")
isAdminCreated = int32(0)
validTLSUsernames = []string{string(sdk.TLSUsernameNone), string(sdk.TLSUsernameCN)}
config Config
provider Provider
sqlPlaceholders []string
internalHashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix}
hashPwdPrefixes = []string{argonPwdPrefix, bcryptPwdPrefix, pbkdf2SHA1Prefix, pbkdf2SHA256Prefix,
pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix, md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
pbkdfPwdPrefixes = []string{pbkdf2SHA1Prefix, pbkdf2SHA256Prefix, pbkdf2SHA512Prefix, pbkdf2SHA256B64SaltPrefix}
pbkdfPwdB64SaltPrefixes = []string{pbkdf2SHA256B64SaltPrefix}
unixPwdPrefixes = []string{md5cryptPwdPrefix, md5cryptApr1PwdPrefix, sha512cryptPwdPrefix}
sharedProviders = []string{PGSQLDataProviderName, MySQLDataProviderName, CockroachDataProviderName}
logSender = "dataProvider"
availabilityTicker *time.Ticker
availabilityTickerDone chan bool
updateCachesTicker *time.Ticker
updateCachesTickerDone chan bool
lastCachesUpdate int64
credentialsDirPath string
sqlTableUsers = "users"
sqlTableFolders = "folders"
sqlTableFoldersMapping = "folders_mapping"
sqlTableAdmins = "admins"
sqlTableAPIKeys = "api_keys"
sqlTableShares = "shares"
sqlTableDefenderHosts = "defender_hosts"
sqlTableDefenderEvents = "defender_events"
sqlTableSchemaVersion = "schema_version"
argon2Params *argon2id.Params
lastLoginMinDelay = 10 * time.Minute
usernameRegex = regexp.MustCompile("^[a-zA-Z0-9-_.~]+$")
tempPath string
)
type schemaVersion struct {
Version int
}
// BcryptOptions defines the options for bcrypt password hashing
type BcryptOptions struct {
Cost int `json:"cost" mapstructure:"cost"`
}
// Argon2Options defines the options for argon2 password hashing
type Argon2Options struct {
Memory uint32 `json:"memory" mapstructure:"memory"`
Iterations uint32 `json:"iterations" mapstructure:"iterations"`
Parallelism uint8 `json:"parallelism" mapstructure:"parallelism"`
}
// PasswordHashing defines the configuration for password hashing
type PasswordHashing struct {
BcryptOptions BcryptOptions `json:"bcrypt_options" mapstructure:"bcrypt_options"`
Argon2Options Argon2Options `json:"argon2_options" mapstructure:"argon2_options"`
// Algorithm to use for hashing passwords. Available algorithms: argon2id, bcrypt. Default: bcrypt
Algo string `json:"algo" mapstructure:"algo"`
}
// PasswordValidationRules defines the password validation rules
type PasswordValidationRules struct {
// MinEntropy defines the minimum password entropy.
// 0 means disabled, any password will be accepted.
// Take a look at the following link for more details
// https://github.com/wagslane/go-password-validator#what-entropy-value-should-i-use
MinEntropy float64 `json:"min_entropy" mapstructure:"min_entropy"`
}
// PasswordValidation defines the password validation rules for admins and protocol users
type PasswordValidation struct {
// Password validation rules for SFTPGo admin users
Admins PasswordValidationRules `json:"admins" mapstructure:"admins"`
// Password validation rules for SFTPGo protocol users
Users PasswordValidationRules `json:"users" mapstructure:"users"`
}
// ObjectsActions defines the action to execute on user create, update, delete for the specified objects
type ObjectsActions struct {
// Valid values are add, update, delete. Empty slice to disable
ExecuteOn []string `json:"execute_on" mapstructure:"execute_on"`
// Valid values are user, admin, api_key
ExecuteFor []string `json:"execute_for" mapstructure:"execute_for"`
// Absolute path to an external program or an HTTP URL
Hook string `json:"hook" mapstructure:"hook"`
}
// ProviderStatus defines the provider status
type ProviderStatus struct {
Driver string `json:"driver"`
IsActive bool `json:"is_active"`
Error string `json:"error"`
}
// Config provider configuration
type Config struct {
// Driver name, must be one of the SupportedProviders
Driver string `json:"driver" mapstructure:"driver"`
// Database name. For driver sqlite this can be the database name relative to the config dir
// or the absolute path to the SQLite database.
Name string `json:"name" mapstructure:"name"`
// Database host
Host string `json:"host" mapstructure:"host"`
// Database port
Port int `json:"port" mapstructure:"port"`
// Database username
Username string `json:"username" mapstructure:"username"`
// Database password
Password string `json:"password" mapstructure:"password"`
// Used for drivers mysql and postgresql.
// 0 disable SSL/TLS connections.
// 1 require ssl.
// 2 set ssl mode to verify-ca for driver postgresql and skip-verify for driver mysql.
// 3 set ssl mode to verify-full for driver postgresql and preferred for driver mysql.
SSLMode int `json:"sslmode" mapstructure:"sslmode"`
// Custom database connection string.
// If not empty this connection string will be used instead of build one using the previous parameters
ConnectionString string `json:"connection_string" mapstructure:"connection_string"`
// prefix for SQL tables
SQLTablesPrefix string `json:"sql_tables_prefix" mapstructure:"sql_tables_prefix"`
// Set the preferred way to track users quota between the following choices:
// 0, disable quota tracking. REST API to scan user dir and update quota will do nothing
// 1, quota is updated each time a user upload or delete a file even if the user has no quota restrictions
// 2, quota is updated each time a user upload or delete a file but only for users with quota restrictions
// and for virtual folders.
// With this configuration the "quota scan" REST API can still be used to periodically update space usage
// for users without quota restrictions
TrackQuota int `json:"track_quota" mapstructure:"track_quota"`
// Sets the maximum number of open connections for mysql and postgresql driver.
// Default 0 (unlimited)
PoolSize int `json:"pool_size" mapstructure:"pool_size"`
// Users default base directory.
// If no home dir is defined while adding a new user, and this value is
// a valid absolute path, then the user home dir will be automatically
// defined as the path obtained joining the base dir and the username
UsersBaseDir string `json:"users_base_dir" mapstructure:"users_base_dir"`
// Actions to execute on objects add, update, delete.
// The supported objects are user, admin, api_key.
// Update action will not be fired for internal updates such as the last login or the user quota fields.
Actions ObjectsActions `json:"actions" mapstructure:"actions"`
// Absolute path to an external program or an HTTP URL to invoke for users authentication.
// Leave empty to use builtin authentication.
// If the authentication succeed the user will be automatically added/updated inside the defined data provider.
// Actions defined for user added/updated will not be executed in this case.
// This method is slower than built-in authentication methods, but it's very flexible as anyone can
// easily write his own authentication hooks.
ExternalAuthHook string `json:"external_auth_hook" mapstructure:"external_auth_hook"`
// ExternalAuthScope defines the scope for the external authentication hook.
// - 0 means all supported authentication scopes, the external hook will be executed for password,
// public key, keyboard interactive authentication and TLS certificates
// - 1 means passwords only
// - 2 means public keys only
// - 4 means keyboard interactive only
// - 8 means TLS certificates only
// you can combine the scopes, for example 3 means password and public key, 5 password and keyboard
// interactive and so on
ExternalAuthScope int `json:"external_auth_scope" mapstructure:"external_auth_scope"`
// CredentialsPath defines the directory for storing user provided credential files such as
// Google Cloud Storage credentials. It can be a path relative to the config dir or an
// absolute path
CredentialsPath string `json:"credentials_path" mapstructure:"credentials_path"`
// Absolute path to an external program or an HTTP URL to invoke just before the user login.
// This program/URL allows to modify or create the user trying to login.
// It is useful if you have users with dynamic fields to update just before the login.
// Please note that if you want to create a new user, the pre-login hook response must
// include all the mandatory user fields.
//
// The pre-login hook must finish within 30 seconds.
//
// If an error happens while executing the "PreLoginHook" then login will be denied.
// PreLoginHook and ExternalAuthHook are mutally exclusive.
// Leave empty to disable.
PreLoginHook string `json:"pre_login_hook" mapstructure:"pre_login_hook"`
// Absolute path to an external program or an HTTP URL to invoke after the user login.
// Based on the configured scope you can choose if notify failed or successful logins
// or both
PostLoginHook string `json:"post_login_hook" mapstructure:"post_login_hook"`
// PostLoginScope defines the scope for the post-login hook.
// - 0 means notify both failed and successful logins
// - 1 means notify failed logins
// - 2 means notify successful logins
PostLoginScope int `json:"post_login_scope" mapstructure:"post_login_scope"`
// Absolute path to an external program or an HTTP URL to invoke just before password
// authentication. This hook allows you to externally check the provided password,
// its main use case is to allow to easily support things like password+OTP for protocols
// without keyboard interactive support such as FTP and WebDAV. You can ask your users
// to login using a string consisting of a fixed password and a One Time Token, you
// can verify the token inside the hook and ask to SFTPGo to verify the fixed part.
CheckPasswordHook string `json:"check_password_hook" mapstructure:"check_password_hook"`
// CheckPasswordScope defines the scope for the check password hook.
// - 0 means all protocols
// - 1 means SSH
// - 2 means FTP
// - 4 means WebDAV
// you can combine the scopes, for example 6 means FTP and WebDAV
CheckPasswordScope int `json:"check_password_scope" mapstructure:"check_password_scope"`
// Defines how the database will be initialized/updated:
// - 0 means automatically
// - 1 means manually using the initprovider sub-command
UpdateMode int `json:"update_mode" mapstructure:"update_mode"`
// PasswordHashing defines the configuration for password hashing
PasswordHashing PasswordHashing `json:"password_hashing" mapstructure:"password_hashing"`
// PreferDatabaseCredentials indicates whether credential files (currently used for Google
// Cloud Storage) should be stored in the database instead of in the directory specified by
// CredentialsPath.
PreferDatabaseCredentials bool `json:"prefer_database_credentials" mapstructure:"prefer_database_credentials"`
// SkipNaturalKeysValidation allows to use any UTF-8 character for natural keys as username, admin name,
// folder name. These keys are used in URIs for REST API and Web admin. By default only unreserved URI
// characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~".
SkipNaturalKeysValidation bool `json:"skip_natural_keys_validation" mapstructure:"skip_natural_keys_validation"`
// PasswordValidation defines the password validation rules
PasswordValidation PasswordValidation `json:"password_validation" mapstructure:"password_validation"`
// Verifying argon2 passwords has a high memory and computational cost,
// by enabling, in memory, password caching you reduce this cost.
PasswordCaching bool `json:"password_caching" mapstructure:"password_caching"`
// DelayedQuotaUpdate defines the number of seconds to accumulate quota updates.
// If there are a lot of close uploads, accumulating quota updates can save you many
// queries to the data provider.
// If you want to track quotas, a scheduled quota update is recommended in any case, the stored
// quota size may be incorrect for several reasons, such as an unexpected shutdown, temporary provider
// failures, file copied outside of SFTPGo, and so on.
// 0 means immediate quota update.
DelayedQuotaUpdate int `json:"delayed_quota_update" mapstructure:"delayed_quota_update"`
// If enabled, a default admin user with username "admin" and password "password" will be created
// on first start.
// You can also create the first admin user by using the web interface or by loading initial data.
CreateDefaultAdmin bool `json:"create_default_admin" mapstructure:"create_default_admin"`
// If the data provider is shared across multiple SFTPGo instances, set this parameter to 1.
// MySQL, PostgreSQL and CockroachDB can be shared, this setting is ignored for other data
// providers. For shared data providers, SFTPGo periodically reloads the latest updated users,
// based on the "updated_at" field, and updates its internal caches if users are updated from
// a different instance. This check, if enabled, is executed every 10 minutes
IsShared int `json:"is_shared" mapstructure:"is_shared"`
}
// IsDefenderSupported returns true if the configured provider supports the defender
func (c *Config) IsDefenderSupported() bool {
switch c.Driver {
case MySQLDataProviderName, PGSQLDataProviderName, CockroachDataProviderName:
return true
default:
return false
}
}
// ActiveTransfer defines an active protocol transfer
type ActiveTransfer struct {
ID int64
Type int
ConnID string
Username string
FolderName string
TruncatedSize int64
CurrentULSize int64
CurrentDLSize int64
CreatedAt int64
UpdatedAt int64
}
// GetKey returns an aggregation key.
// The same key will be returned for similar transfers
func (t *ActiveTransfer) GetKey() string {
return fmt.Sprintf("%v%v%v", t.Username, t.FolderName, t.Type)
}
// DefenderEntry defines a defender entry
type DefenderEntry struct {
ID int64 `json:"-"`
IP string `json:"ip"`
Score int `json:"score,omitempty"`
BanTime time.Time `json:"ban_time,omitempty"`
}
// GetID returns an unique ID for a defender entry
func (d *DefenderEntry) GetID() string {
return hex.EncodeToString([]byte(d.IP))
}
// GetBanTime returns the ban time for a defender entry as string
func (d *DefenderEntry) GetBanTime() string {
if d.BanTime.IsZero() {
return ""
}
return d.BanTime.UTC().Format(time.RFC3339)
}
// MarshalJSON returns the JSON encoding of a DefenderEntry.
func (d *DefenderEntry) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
ID string `json:"id"`
IP string `json:"ip"`
Score int `json:"score,omitempty"`
BanTime string `json:"ban_time,omitempty"`
}{
ID: d.GetID(),
IP: d.IP,
Score: d.Score,
BanTime: d.GetBanTime(),
})
}
// BackupData defines the structure for the backup/restore files
type BackupData struct {
Users []User `json:"users"`
Folders []vfs.BaseVirtualFolder `json:"folders"`
Admins []Admin `json:"admins"`
APIKeys []APIKey `json:"api_keys"`
Shares []Share `json:"shares"`
Version int `json:"version"`
}
// HasFolder returns true if the folder with the given name is included
func (d *BackupData) HasFolder(name string) bool {
for _, folder := range d.Folders {
if folder.Name == name {
return true
}
}
return false
}
type checkPasswordRequest struct {
Username string `json:"username"`
IP string `json:"ip"`
Password string `json:"password"`
Protocol string `json:"protocol"`
}
type checkPasswordResponse struct {
// 0 KO, 1 OK, 2 partial success, -1 not executed
Status int `json:"status"`
// for status = 2 this is the password to check against the one stored
// inside the SFTPGo data provider
ToVerify string `json:"to_verify"`
}
// GetQuotaTracking returns the configured mode for user's quota tracking
func GetQuotaTracking() int {
return config.TrackQuota
}
// HasUsersBaseDir returns true if users base dir is set
func HasUsersBaseDir() bool {
return config.UsersBaseDir != ""
}
// Provider defines the interface that data providers must implement.
type Provider interface {
validateUserAndPass(username, password, ip, protocol string) (User, error)
validateUserAndPubKey(username string, pubKey []byte) (User, string, error)
validateUserAndTLSCert(username, protocol string, tlsCert *x509.Certificate) (User, error)
updateQuota(username string, filesAdd int, sizeAdd int64, reset bool) error
getUsedQuota(username string) (int, int64, error)
userExists(username string) (User, error)
addUser(user *User) error
updateUser(user *User) error
deleteUser(user *User) error
getUsers(limit int, offset int, order string) ([]User, error)
dumpUsers() ([]User, error)
getRecentlyUpdatedUsers(after int64) ([]User, error)
getUsersForQuotaCheck(toFetch map[string]bool) ([]User, error)
updateLastLogin(username string) error
updateAdminLastLogin(username string) error
setUpdatedAt(username string)
getFolders(limit, offset int, order string) ([]vfs.BaseVirtualFolder, error)
getFolderByName(name string) (vfs.BaseVirtualFolder, error)
addFolder(folder *vfs.BaseVirtualFolder) error
updateFolder(folder *vfs.BaseVirtualFolder) error
deleteFolder(folder *vfs.BaseVirtualFolder) error
updateFolderQuota(name string, filesAdd int, sizeAdd int64, reset bool) error
getUsedFolderQuota(name string) (int, int64, error)
dumpFolders() ([]vfs.BaseVirtualFolder, error)
adminExists(username string) (Admin, error)
addAdmin(admin *Admin) error
updateAdmin(admin *Admin) error
deleteAdmin(admin *Admin) error
getAdmins(limit int, offset int, order string) ([]Admin, error)
dumpAdmins() ([]Admin, error)
validateAdminAndPass(username, password, ip string) (Admin, error)
apiKeyExists(keyID string) (APIKey, error)
addAPIKey(apiKey *APIKey) error
updateAPIKey(apiKey *APIKey) error
deleteAPIKey(apiKey *APIKey) error
getAPIKeys(limit int, offset int, order string) ([]APIKey, error)
dumpAPIKeys() ([]APIKey, error)
updateAPIKeyLastUse(keyID string) error
shareExists(shareID, username string) (Share, error)
addShare(share *Share) error
updateShare(share *Share) error
deleteShare(share *Share) error
getShares(limit int, offset int, order, username string) ([]Share, error)
dumpShares() ([]Share, error)
updateShareLastUse(shareID string, numTokens int) error
getDefenderHosts(from int64, limit int) ([]DefenderEntry, error)
getDefenderHostByIP(ip string, from int64) (DefenderEntry, error)
isDefenderHostBanned(ip string) (DefenderEntry, error)
updateDefenderBanTime(ip string, minutes int) error
deleteDefenderHost(ip string) error
addDefenderEvent(ip string, score int) error
setDefenderBanTime(ip string, banTime int64) error
cleanupDefender(from int64) error
checkAvailability() error
close() error
reloadConfig() error
initializeDatabase() error
migrateDatabase() error
revertDatabase(targetVersion int) error
resetDatabase() error
}
// SetTempPath sets the path for temporary files
func SetTempPath(fsPath string) {
tempPath = fsPath
}
// Initialize the data provider.
// An error is returned if the configured driver is invalid or if the data provider cannot be initialized
func Initialize(cnf Config, basePath string, checkAdmins bool) error {
var err error
config = cnf
if filepath.IsAbs(config.CredentialsPath) {
credentialsDirPath = config.CredentialsPath
} else {
credentialsDirPath = filepath.Join(basePath, config.CredentialsPath)
}
vfs.SetCredentialsDirPath(credentialsDirPath)
if err = initializeHashingAlgo(&cnf); err != nil {
return err
}
if err = validateHooks(); err != nil {
return err
}
err = createProvider(basePath)
if err != nil {
return err
}
if cnf.UpdateMode == 0 {
err = provider.initializeDatabase()
if err != nil && err != ErrNoInitRequired {
logger.WarnToConsole("Unable to initialize data provider: %v", err)
providerLog(logger.LevelError, "Unable to initialize data provider: %v", err)
return err
}
if err == nil {
logger.DebugToConsole("Data provider successfully initialized")
}
err = provider.migrateDatabase()
if err != nil && err != ErrNoInitRequired {
providerLog(logger.LevelError, "database migration error: %v", err)
return err
}
if checkAdmins && cnf.CreateDefaultAdmin {
err = checkDefaultAdmin()
if err != nil {
providerLog(logger.LevelError, "erro checking the default admin: %v", err)
return err
}
}
} else {
providerLog(logger.LevelInfo, "database initialization/migration skipped, manual mode is configured")
}
admins, err := provider.getAdmins(1, 0, OrderASC)
if err != nil {
return err
}
atomic.StoreInt32(&isAdminCreated, int32(len(admins)))
startAvailabilityTimer()
startUpdateCachesTimer()
delayedQuotaUpdater.start()
return nil
}
func validateHooks() error {
var hooks []string
if config.PreLoginHook != "" && !strings.HasPrefix(config.PreLoginHook, "http") {
hooks = append(hooks, config.PreLoginHook)
}
if config.ExternalAuthHook != "" && !strings.HasPrefix(config.ExternalAuthHook, "http") {
hooks = append(hooks, config.ExternalAuthHook)
}
if config.PostLoginHook != "" && !strings.HasPrefix(config.PostLoginHook, "http") {
hooks = append(hooks, config.PostLoginHook)
}
if config.CheckPasswordHook != "" && !strings.HasPrefix(config.CheckPasswordHook, "http") {
hooks = append(hooks, config.CheckPasswordHook)
}
for _, hook := range hooks {
if !filepath.IsAbs(hook) {
return fmt.Errorf("invalid hook: %#v must be an absolute path", hook)
}
_, err := os.Stat(hook)
if err != nil {
providerLog(logger.LevelError, "invalid hook: %v", err)
return err
}
}
return nil
}
func initializeHashingAlgo(cnf *Config) error {
argon2Params = &argon2id.Params{
Memory: cnf.PasswordHashing.Argon2Options.Memory,
Iterations: cnf.PasswordHashing.Argon2Options.Iterations,
Parallelism: cnf.PasswordHashing.Argon2Options.Parallelism,
SaltLength: 16,
KeyLength: 32,
}
if config.PasswordHashing.Algo == HashingAlgoBcrypt {
if config.PasswordHashing.BcryptOptions.Cost > bcrypt.MaxCost {
err := fmt.Errorf("invalid bcrypt cost %v, max allowed %v", config.PasswordHashing.BcryptOptions.Cost, bcrypt.MaxCost)
logger.WarnToConsole("Unable to initialize data provider: %v", err)
providerLog(logger.LevelError, "Unable to initialize data provider: %v", err)
return err
}
}
return nil
}
func validateSQLTablesPrefix() error {
if config.SQLTablesPrefix != "" {
for _, char := range config.SQLTablesPrefix {
if !strings.Contains(sqlPrefixValidChars, strings.ToLower(string(char))) {
return errors.New("invalid sql_tables_prefix only chars in range 'a..z', 'A..Z', '0-9' and '_' are allowed")
}
}
sqlTableUsers = config.SQLTablesPrefix + sqlTableUsers
sqlTableFolders = config.SQLTablesPrefix + sqlTableFolders
sqlTableFoldersMapping = config.SQLTablesPrefix + sqlTableFoldersMapping
sqlTableAdmins = config.SQLTablesPrefix + sqlTableAdmins
sqlTableAPIKeys = config.SQLTablesPrefix + sqlTableAPIKeys
sqlTableShares = config.SQLTablesPrefix + sqlTableShares
sqlTableSchemaVersion = config.SQLTablesPrefix + sqlTableSchemaVersion
providerLog(logger.LevelDebug, "sql table for users %#v, folders %#v folders mapping %#v admins %#v "+
"api keys %#v shares %#v schema version %#v", sqlTableUsers, sqlTableFolders, sqlTableFoldersMapping,
sqlTableAdmins, sqlTableAPIKeys, sqlTableShares, sqlTableSchemaVersion)
}
return nil
}
func checkDefaultAdmin() error {
admins, err := provider.getAdmins(1, 0, OrderASC)
if err != nil {
return err
}
if len(admins) > 0 {
return nil
}
logger.Debug(logSender, "", "no admins found, try to create the default one")
// we need to create the default admin
admin := &Admin{}
if err := admin.setFromEnv(); err != nil {
return err
}
return provider.addAdmin(admin)
}
// InitializeDatabase creates the initial database structure
func InitializeDatabase(cnf Config, basePath string) error {
config = cnf
if filepath.IsAbs(config.CredentialsPath) {
credentialsDirPath = config.CredentialsPath
} else {
credentialsDirPath = filepath.Join(basePath, config.CredentialsPath)
}
err := createProvider(basePath)
if err != nil {
return err
}
err = provider.initializeDatabase()
if err != nil && err != ErrNoInitRequired {
return err
}
return provider.migrateDatabase()
}
// RevertDatabase restores schema and/or data to a previous version
func RevertDatabase(cnf Config, basePath string, targetVersion int) error {
config = cnf
if filepath.IsAbs(config.CredentialsPath) {
credentialsDirPath = config.CredentialsPath
} else {
credentialsDirPath = filepath.Join(basePath, config.CredentialsPath)
}
err := createProvider(basePath)
if err != nil {
return err
}
err = provider.initializeDatabase()
if err != nil && err != ErrNoInitRequired {
return err
}
return provider.revertDatabase(targetVersion)
}
// ResetDatabase restores schema and/or data to a previous version
func ResetDatabase(cnf Config, basePath string) error {
config = cnf
if filepath.IsAbs(config.CredentialsPath) {
credentialsDirPath = config.CredentialsPath
} else {
credentialsDirPath = filepath.Join(basePath, config.CredentialsPath)
}
if err := createProvider(basePath); err != nil {
return err
}
return provider.resetDatabase()
}
// CheckAdminAndPass validates the given admin and password connecting from ip
func CheckAdminAndPass(username, password, ip string) (Admin, error) {
return provider.validateAdminAndPass(username, password, ip)
}
// CheckCachedUserCredentials checks the credentials for a cached user
func CheckCachedUserCredentials(user *CachedUser, password, loginMethod, protocol string, tlsCert *x509.Certificate) error {
if loginMethod != LoginMethodPassword {
_, err := checkUserAndTLSCertificate(&user.User, protocol, tlsCert)
if err != nil {
return err
}
if loginMethod == LoginMethodTLSCertificate {
if !user.User.IsLoginMethodAllowed(LoginMethodTLSCertificate, nil) {
return fmt.Errorf("certificate login method is not allowed for user %#v", user.User.Username)
}
return nil
}
}
if err := user.User.CheckLoginConditions(); err != nil {
return err
}
if password == "" {
return ErrInvalidCredentials
}
if user.Password != "" {
if password == user.Password {
return nil
}
} else {
if ok, _ := isPasswordOK(&user.User, password); ok {
return nil
}
}
return ErrInvalidCredentials
}
// CheckCompositeCredentials checks multiple credentials.
// WebDAV users can send both a password and a TLS certificate within the same request
func CheckCompositeCredentials(username, password, ip, loginMethod, protocol string, tlsCert *x509.Certificate) (User, string, error) {
if loginMethod == LoginMethodPassword {
user, err := CheckUserAndPass(username, password, ip, protocol)
return user, loginMethod, err
}
user, err := CheckUserBeforeTLSAuth(username, ip, protocol, tlsCert)
if err != nil {
return user, loginMethod, err
}
if !user.IsTLSUsernameVerificationEnabled() {
// for backward compatibility with 2.0.x we only check the password and change the login method here
// in future updates we have to return an error
user, err := CheckUserAndPass(username, password, ip, protocol)
return user, LoginMethodPassword, err
}
user, err = checkUserAndTLSCertificate(&user, protocol, tlsCert)
if err != nil {
return user, loginMethod, err
}
if loginMethod == LoginMethodTLSCertificate && !user.IsLoginMethodAllowed(LoginMethodTLSCertificate, nil) {
return user, loginMethod, fmt.Errorf("certificate login method is not allowed for user %#v", user.Username)
}
if loginMethod == LoginMethodTLSCertificateAndPwd {
if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) {
user, err = doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword)
} else if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
user, err = doExternalAuth(username, password, nil, "", ip, protocol, nil)
} else if config.PreLoginHook != "" {
user, err = executePreLoginHook(username, LoginMethodPassword, ip, protocol)
}
if err != nil {
return user, loginMethod, err
}
user, err = checkUserAndPass(&user, password, ip, protocol)
}
return user, loginMethod, err
}
// CheckUserBeforeTLSAuth checks if a user exits before trying mutual TLS
func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
if plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) {
return doPluginAuth(username, "", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate)
}
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {
return doExternalAuth(username, "", nil, "", ip, protocol, tlsCert)
}
if config.PreLoginHook != "" {
return executePreLoginHook(username, LoginMethodTLSCertificate, ip, protocol)
}
return UserExists(username)
}
// CheckUserAndTLSCert returns the SFTPGo user with the given username and check if the
// given TLS certificate allow authentication without password
func CheckUserAndTLSCert(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) {
if plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) {
user, err := doPluginAuth(username, "", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate)
if err != nil {
return user, err
}
return checkUserAndTLSCertificate(&user, protocol, tlsCert)
}
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&8 != 0) {
user, err := doExternalAuth(username, "", nil, "", ip, protocol, tlsCert)
if err != nil {
return user, err
}
return checkUserAndTLSCertificate(&user, protocol, tlsCert)
}
if config.PreLoginHook != "" {
user, err := executePreLoginHook(username, LoginMethodTLSCertificate, ip, protocol)
if err != nil {
return user, err
}
return checkUserAndTLSCertificate(&user, protocol, tlsCert)
}
return provider.validateUserAndTLSCert(username, protocol, tlsCert)
}
// CheckUserAndPass retrieves the SFTPGo user with the given username and password if a match is found or an error
func CheckUserAndPass(username, password, ip, protocol string) (User, error) {
if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) {
user, err := doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword)
if err != nil {
return user, err
}
return checkUserAndPass(&user, password, ip, protocol)
}
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {
user, err := doExternalAuth(username, password, nil, "", ip, protocol, nil)
if err != nil {
return user, err
}
return checkUserAndPass(&user, password, ip, protocol)
}
if config.PreLoginHook != "" {
user, err := executePreLoginHook(username, LoginMethodPassword, ip, protocol)
if err != nil {
return user, err
}
return checkUserAndPass(&user, password, ip, protocol)
}
return provider.validateUserAndPass(username, password, ip, protocol)
}
// CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error
func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string) (User, string, error) {
if plugin.Handler.HasAuthScope(plugin.AuthScopePublicKey) {
user, err := doPluginAuth(username, "", pubKey, ip, protocol, nil, plugin.AuthScopePublicKey)
if err != nil {
return user, "", err
}
return checkUserAndPubKey(&user, pubKey)
}
if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&2 != 0) {
user, err := doExternalAuth(username, "", pubKey, "", ip, protocol, nil)
if err != nil {
return user, "", err
}
return checkUserAndPubKey(&user, pubKey)
}
if config.PreLoginHook != "" {
user, err := executePreLoginHook(username, SSHLoginMethodPublicKey, ip, protocol)
if err != nil {
return user, "", err
}
return checkUserAndPubKey(&user, pubKey)
}
return provider.validateUserAndPubKey(username, pubKey)
}
// CheckKeyboardInteractiveAuth checks the keyboard interactive authentication and returns
// the authenticated user or an error
func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) {
var user User
var err error
if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) {
user, err = doPluginAuth(username, "", nil, ip, protocol, nil, plugin.AuthScopeKeyboardInteractive)
} else if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) {
user, err = doExternalAuth(username, "", nil, "1", ip, protocol, nil)
} else if config.PreLoginHook != "" {
user, err = executePreLoginHook(username, SSHLoginMethodKeyboardInteractive, ip, protocol)
} else {
user, err = provider.userExists(username)
}
if err != nil {
return user, err
}
return doKeyboardInteractiveAuth(&user, authHook, client, ip, protocol)
}
// GetDefenderHosts returns hosts that are banned or for which some violations have been detected
func GetDefenderHosts(from int64, limit int) ([]DefenderEntry, error) {
return provider.getDefenderHosts(from, limit)
}
// GetDefenderHostByIP returns a defender host by ip, if any
func GetDefenderHostByIP(ip string, from int64) (DefenderEntry, error) {
return provider.getDefenderHostByIP(ip, from)
}
// IsDefenderHostBanned returns a defender entry and no error if the specified host is banned
func IsDefenderHostBanned(ip string) (DefenderEntry, error) {
return provider.isDefenderHostBanned(ip)
}
// UpdateDefenderBanTime increments ban time for the specified ip
func UpdateDefenderBanTime(ip string, minutes int) error {
return provider.updateDefenderBanTime(ip, minutes)
}
// DeleteDefenderHost removes the specified IP from the defender lists
func DeleteDefenderHost(ip string) error {
return provider.deleteDefenderHost(ip)
}
// AddDefenderEvent adds an event for the given IP with the given score
// and returns the host with the updated score
func AddDefenderEvent(ip string, score int, from int64) (DefenderEntry, error) {
if err := provider.addDefenderEvent(ip, score); err != nil {
return DefenderEntry{}, err
}
return provider.getDefenderHostByIP(ip, from)
}
// SetDefenderBanTime sets the ban time for the specified IP
func SetDefenderBanTime(ip string, banTime int64) error {
return provider.setDefenderBanTime(ip, banTime)
}
// CleanupDefender removes events and hosts older than "from" from the data provider
func CleanupDefender(from int64) error {
return provider.cleanupDefender(from)
}
// UpdateShareLastUse updates the LastUseAt and UsedTokens for the given share
func UpdateShareLastUse(share *Share, numTokens int) error {
return provider.updateShareLastUse(share.ShareID, numTokens)
}
// UpdateAPIKeyLastUse updates the LastUseAt field for the given API key
func UpdateAPIKeyLastUse(apiKey *APIKey) error {
lastUse := util.GetTimeFromMsecSinceEpoch(apiKey.LastUseAt)