OIDC: execute pre-login hook after IDP authentication
so the SFTPGo users can be auto-created using the hook Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
f1a255aa6c
commit
c6b8644828
22 changed files with 313 additions and 108 deletions
|
@ -83,6 +83,7 @@ const (
|
|||
ProtocolHTTP = "HTTP"
|
||||
ProtocolHTTPShare = "HTTPShare"
|
||||
ProtocolDataRetention = "DataRetention"
|
||||
ProtocolOIDC = "OIDC"
|
||||
)
|
||||
|
||||
// Upload modes
|
||||
|
@ -128,7 +129,7 @@ var (
|
|||
periodicTimeoutTicker *time.Ticker
|
||||
periodicTimeoutTickerDone chan bool
|
||||
supportedProtocols = []string{ProtocolSFTP, ProtocolSCP, ProtocolSSH, ProtocolFTP, ProtocolWebDAV,
|
||||
ProtocolHTTP, ProtocolHTTPShare}
|
||||
ProtocolHTTP, ProtocolHTTPShare, ProtocolOIDC}
|
||||
disconnHookProtocols = []string{ProtocolSFTP, ProtocolSCP, ProtocolSSH, ProtocolFTP}
|
||||
// the map key is the protocol, for each protocol we can have multiple rate limiters
|
||||
rateLimiters map[string][]*rateLimiter
|
||||
|
|
|
@ -1250,7 +1250,7 @@ func (c *BaseConnection) IsNotExistError(err error) bool {
|
|||
switch c.protocol {
|
||||
case ProtocolSFTP:
|
||||
return errors.Is(err, sftp.ErrSSHFxNoSuchFile)
|
||||
case ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolHTTPShare, ProtocolDataRetention:
|
||||
case ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolOIDC, ProtocolHTTPShare, ProtocolDataRetention:
|
||||
return errors.Is(err, os.ErrNotExist)
|
||||
default:
|
||||
return errors.Is(err, ErrNotExist)
|
||||
|
@ -1272,7 +1272,7 @@ func (c *BaseConnection) GetPermissionDeniedError() error {
|
|||
switch c.protocol {
|
||||
case ProtocolSFTP:
|
||||
return sftp.ErrSSHFxPermissionDenied
|
||||
case ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolHTTPShare, ProtocolDataRetention:
|
||||
case ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolOIDC, ProtocolHTTPShare, ProtocolDataRetention:
|
||||
return os.ErrPermission
|
||||
default:
|
||||
return ErrPermissionDenied
|
||||
|
@ -1284,7 +1284,7 @@ func (c *BaseConnection) GetNotExistError() error {
|
|||
switch c.protocol {
|
||||
case ProtocolSFTP:
|
||||
return sftp.ErrSSHFxNoSuchFile
|
||||
case ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolHTTPShare, ProtocolDataRetention:
|
||||
case ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolOIDC, ProtocolHTTPShare, ProtocolDataRetention:
|
||||
return os.ErrNotExist
|
||||
default:
|
||||
return ErrNotExist
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
"github.com/drakkan/sftpgo/v2/dataprovider"
|
||||
"github.com/drakkan/sftpgo/v2/kms"
|
||||
"github.com/drakkan/sftpgo/v2/util"
|
||||
"github.com/drakkan/sftpgo/v2/vfs"
|
||||
)
|
||||
|
||||
|
@ -262,13 +263,14 @@ func TestUpdateQuotaAfterRename(t *testing.T) {
|
|||
func TestErrorsMapping(t *testing.T) {
|
||||
fs := vfs.NewOsFs("", os.TempDir(), "")
|
||||
conn := NewBaseConnection("", ProtocolSFTP, "", "", dataprovider.User{BaseUser: sdk.BaseUser{HomeDir: os.TempDir()}})
|
||||
osErrorsProtocols := []string{ProtocolWebDAV, ProtocolFTP, ProtocolHTTP, ProtocolHTTPShare,
|
||||
ProtocolDataRetention, ProtocolOIDC}
|
||||
for _, protocol := range supportedProtocols {
|
||||
conn.SetProtocol(protocol)
|
||||
err := conn.GetFsError(fs, os.ErrNotExist)
|
||||
if protocol == ProtocolSFTP {
|
||||
assert.ErrorIs(t, err, sftp.ErrSSHFxNoSuchFile)
|
||||
} else if protocol == ProtocolWebDAV || protocol == ProtocolFTP || protocol == ProtocolHTTP ||
|
||||
protocol == ProtocolHTTPShare || protocol == ProtocolDataRetention {
|
||||
} else if util.IsStringInSlice(protocol, osErrorsProtocols) {
|
||||
assert.EqualError(t, err, os.ErrNotExist.Error())
|
||||
} else {
|
||||
assert.EqualError(t, err, ErrNotExist.Error())
|
||||
|
|
|
@ -1050,6 +1050,17 @@ func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.Keyboard
|
|||
return doKeyboardInteractiveAuth(&user, authHook, client, ip, protocol)
|
||||
}
|
||||
|
||||
// GetUserAfterIDPAuth returns the SFTPGo user with the specified username
|
||||
// after a successful authentication with an external identity provider.
|
||||
// If a pre-login hook is defined it will be executed so the SFTPGo user
|
||||
// can be created if it does not exist
|
||||
func GetUserAfterIDPAuth(username, ip, protocol string) (User, error) {
|
||||
if config.PreLoginHook != "" {
|
||||
return executePreLoginHook(username, LoginMethodIDP, ip, protocol)
|
||||
}
|
||||
return UserExists(username)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
@ -70,6 +70,7 @@ const (
|
|||
SSHLoginMethodKeyAndKeyboardInt = "publickey+keyboard-interactive"
|
||||
LoginMethodTLSCertificate = "TLSCertificate"
|
||||
LoginMethodTLSCertificateAndPwd = "TLSCertificate+password"
|
||||
LoginMethodIDP = "IDP"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -6,9 +6,9 @@ To enable dynamic user modification, you must set the absolute path of your prog
|
|||
The external program can read the following environment variables to get info about the user trying to login:
|
||||
|
||||
- `SFTPGO_LOGIND_USER`, it contains the user trying to login serialized as JSON. A JSON serialized user id equal to zero means the user does not exist inside SFTPGo
|
||||
- `SFTPGO_LOGIND_METHOD`, possible values are: `password`, `publickey`, `keyboard-interactive`, `TLSCertificate`
|
||||
- `SFTPGO_LOGIND_METHOD`, possible values are: `password`, `publickey`, `keyboard-interactive`, `TLSCertificate`, `IDP` (external identity provider)
|
||||
- `SFTPGO_LOGIND_IP`, ip address of the user trying to login
|
||||
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`
|
||||
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`, `OIDC` (OpenID Connect)
|
||||
|
||||
The program must write, on its standard output:
|
||||
|
||||
|
@ -35,6 +35,8 @@ If an error happens while executing the hook then login will be denied.
|
|||
"Dynamic user creation or modification" and "External Authentication" are mutually exclusive, they are quite similar, the difference is that "External Authentication" returns an already authenticated user while using "Dynamic users modification" you simply create or update a user. The authentication will be checked inside SFTPGo.
|
||||
In other words while using "External Authentication" the external program receives the credentials of the user trying to login (for example the cleartext password) and it needs to validate them. While using "Dynamic users modification" the pre-login program receives the user stored inside the dataprovider (it includes the hashed password if any) and it can modify it, after the modification SFTPGo will check the credentials of the user trying to login.
|
||||
|
||||
For SFTPGo users (not admins) authenticating using an external identity provider such as OpenID Connect, the pre-login hook will be executed after a successful authentication against the external IDP so that you can create/update the SFTPGo user matching the one authenticated against the identity provider. This is the only case where the pre-login hook is executed even if an external authentication hook is defined.
|
||||
|
||||
You can disable the hook on a per-user basis.
|
||||
|
||||
Let's see a very basic example. Our sample program will grant access to the existing user `test_user` only in the time range 10:00-18:00. Other users will not be modified since the program will terminate with no output.
|
||||
|
|
|
@ -252,7 +252,7 @@ The configuration file contains the following sections:
|
|||
- `sts_seconds`, integer. Defines the max-age of the `Strict-Transport-Security` header. This header will be included for `https` responses or for HTTP request if the request includes a defined HTTPS proxy header. Default: `0`, which would NOT include the header.
|
||||
- `sts_include_subdomains`, boolean. Set to `true`, the `includeSubdomains` will be appended to the `Strict-Transport-Security` header. Default: `false`.
|
||||
- `sts_preload`, boolean. Set to true, the `preload` flag will be appended to the `Strict-Transport-Security` header. Default: `false`.
|
||||
- `content_type_nosniff`, boolean. Set to `true` to add the `X-Content-Type-Options` header with the value `nosniff` Default: `false`.
|
||||
- `content_type_nosniff`, boolean. Set to `true` to add the `X-Content-Type-Options` header with the value `nosniff`. Default: `false`.
|
||||
- `content_security_policy`, string. Allows to set the `Content-Security-Policy` header value. Default: blank.
|
||||
- `permissions_policy`, string. Allows to set the `Permissions-Policy` header value. Default: blank.
|
||||
- `cross_origin_opener_policy`, string. Allows to set the `Cross-Origin-Opener-Policy` header value. Default: blank.
|
||||
|
|
|
@ -97,3 +97,5 @@ And the following is an example ID token which allows the SFTPGo user `user1` to
|
|||
"preferred_username": "user1"
|
||||
}
|
||||
```
|
||||
|
||||
SFTPGo users (not admins) can be created/updated after successful OpenID authentication by defining a [pre-login hook](./dynamic-user-mod.md).
|
||||
|
|
|
@ -9,7 +9,7 @@ The `post_connect_hook` can be defined as the absolute path of your program or a
|
|||
If the hook defines an external program it can read the following environment variables:
|
||||
|
||||
- `SFTPGO_CONNECTION_IP`
|
||||
- `SFTPGO_CONNECTION_PROTOCOL`
|
||||
- `SFTPGO_CONNECTION_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`, `OIDC` (OpenID Connect)
|
||||
|
||||
If the external command completes with a zero exit status the connection will be accepted otherwise rejected.
|
||||
|
||||
|
@ -19,7 +19,7 @@ The program must finish within 20 seconds.
|
|||
If the hook defines an HTTP URL then this URL will be invoked as HTTP GET with the following query parameters:
|
||||
|
||||
- `ip`
|
||||
- `protocol`
|
||||
- `protocol`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`, `OIDC` (OpenID Connect)
|
||||
|
||||
The connection is accepted if the HTTP response code is `200` otherwise rejected.
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ The `post_disconnect_hook` can be defined as the absolute path of your program o
|
|||
If the hook defines an external program it can read the following environment variables:
|
||||
|
||||
- `SFTPGO_CONNECTION_IP`
|
||||
- `SFTPGO_CONNECTION_PROTOCOL`
|
||||
- `SFTPGO_CONNECTION_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`, `OIDC` (OpenID Connect)
|
||||
- `SFTPGO_CONNECTION_USERNAME`, can be empty if the channel is closed before user authentication
|
||||
- `SFTPGO_CONNECTION_DURATION`, connection duration in milliseconds
|
||||
|
||||
|
@ -19,7 +19,7 @@ The program must finish within 20 seconds.
|
|||
If the hook defines an HTTP URL then this URL will be invoked as HTTP GET with the following query parameters:
|
||||
|
||||
- `ip`
|
||||
- `protocol`
|
||||
- `protocol`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`, `OIDC` (OpenID Connect)
|
||||
- `username`, can be empty if the channel is closed before user authentication
|
||||
- `connection_duration`, connection duration in milliseconds
|
||||
|
||||
|
|
|
@ -10,9 +10,9 @@ If the hook defines an external program it can reads the following environment v
|
|||
|
||||
- `SFTPGO_LOGIND_USER`, it contains the user serialized as JSON. The username is empty if the connection is closed for authentication timeout
|
||||
- `SFTPGO_LOGIND_IP`
|
||||
- `SFTPGO_LOGIND_METHOD`, possible values are `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive`, `TLSCertificate`, `TLSCertificate+password` or `no_auth_tryed`
|
||||
- `SFTPGO_LOGIND_METHOD`, possible values are `publickey`, `password`, `keyboard-interactive`, `publickey+password`, `publickey+keyboard-interactive`, `TLSCertificate`, `TLSCertificate+password` or `no_auth_tryed`, `IDP` (external identity provider)
|
||||
- `SFTPGO_LOGIND_STATUS`, 1 means login OK, 0 login KO
|
||||
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`
|
||||
- `SFTPGO_LOGIND_PROTOCOL`, possible values are `SSH`, `FTP`, `DAV`, `HTTP`, `OIDC` (OpenID Connect)
|
||||
|
||||
Previous global environment variables aren't cleared when the script is called.
|
||||
The program must finish within 20 seconds.
|
||||
|
|
9
go.mod
9
go.mod
|
@ -3,14 +3,14 @@ module github.com/drakkan/sftpgo/v2
|
|||
go 1.17
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.20.0
|
||||
cloud.google.com/go/storage v1.21.0
|
||||
github.com/Azure/azure-storage-blob-go v0.14.0
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/aws/aws-sdk-go v1.43.0
|
||||
github.com/aws/aws-sdk-go v1.43.2
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.8
|
||||
github.com/coreos/go-oidc/v3 v3.1.0
|
||||
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001
|
||||
github.com/fclairamb/ftpserverlib v0.17.1-0.20220212161409-5157f18d716f
|
||||
github.com/fclairamb/go-log v0.2.0
|
||||
github.com/go-chi/chi/v5 v5.0.8-0.20220103230436-7dbe9a0bd10f
|
||||
|
@ -128,7 +128,7 @@ require (
|
|||
golang.org/x/tools v0.1.9 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220217155828-d576998c0009 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c // indirect
|
||||
google.golang.org/grpc v1.44.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.4 // indirect
|
||||
|
@ -138,7 +138,6 @@ require (
|
|||
)
|
||||
|
||||
replace (
|
||||
github.com/eikenb/pipeat => github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639
|
||||
github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9
|
||||
golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20220215181150-74469fa99b22
|
||||
golang.org/x/net => github.com/drakkan/net v0.0.0-20220130095023-bd85f1236c34
|
||||
|
|
17
go.sum
17
go.sum
|
@ -72,8 +72,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX
|
|||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
cloud.google.com/go/storage v1.16.1/go.mod h1:LaNorbty3ehnU3rEjXSNV/NRgQA0O8Y+uh6bPe5UOk4=
|
||||
cloud.google.com/go/storage v1.20.0 h1:kv3rQ3clEQdxqokkCCgQo+bxPqcuXiROjxvnKb8Oqdk=
|
||||
cloud.google.com/go/storage v1.20.0/go.mod h1:TiC1o6FxNCG8y5gB7rqCsFZCIYPMPZCO81ppOoEPLGI=
|
||||
cloud.google.com/go/storage v1.21.0 h1:HwnT2u2D309SFDHQII6m18HlrCi3jAXhUMTLOWXYH14=
|
||||
cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA=
|
||||
cloud.google.com/go/trace v0.1.0/go.mod h1:wxEwsoeRVPbeSkt7ZC9nWCgmoKQRAoySN7XHW2AmI7g=
|
||||
contrib.go.opencensus.io/exporter/aws v0.0.0-20200617204711-c478e41e60e9/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA=
|
||||
contrib.go.opencensus.io/exporter/stackdriver v0.13.8/go.mod h1:huNtlWx75MwO7qMs0KrMxPZXzNNWebav1Sq/pm02JdQ=
|
||||
|
@ -143,8 +143,8 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
|
|||
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.43.0 h1:y4UrPbxU/mIL08qksVPE/nwH9IXuC1udjOaNyhEe+pI=
|
||||
github.com/aws/aws-sdk-go v1.43.0/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
|
||||
github.com/aws/aws-sdk-go v1.43.2 h1:T6LuKCNu8CYXXDn3xJoldh8FbdvuVH7C9aSuLNrlht0=
|
||||
github.com/aws/aws-sdk-go v1.43.2/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.4.0/go.mod h1:dgGR+Qq7Wjcd4AOAW5Rf5Tnv3+x7ed6kETXyS9WCuAY=
|
||||
|
@ -224,8 +224,8 @@ github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHP
|
|||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9/go.mod h1:2lmrmq866uF2tnje75wQHzmPXhmSWUt7Gyx2vgK1RCU=
|
||||
github.com/drakkan/net v0.0.0-20220130095023-bd85f1236c34 h1:DRayAKtBRaVU3jg58b/HCbkRleByBD5q6NkN1wcJ2RU=
|
||||
github.com/drakkan/net v0.0.0-20220130095023-bd85f1236c34/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639 h1:8tfGdb4kg/YCvAbIrsMazgoNtnqdOqQVDKW12uUCuuU=
|
||||
github.com/drakkan/pipeat v0.0.0-20210805162858-70e57fa8a639/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 h1:/ZshrfQzayqRSBDodmp3rhNCHJCff+utvgBuWRbiqu4=
|
||||
github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001/go.mod h1:kltMsfRMTHSFdMbK66XdS8mfMW77+FZA1fGY1xYMF84=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
|
@ -1177,8 +1177,9 @@ google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ6
|
|||
google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220217155828-d576998c0009 h1:8QEZX8dJDqdCxQVLRWzEKGOkOzuDx0AU4+bQX6LwmU4=
|
||||
google.golang.org/genproto v0.0.0-20220217155828-d576998c0009/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c h1:TU4rFa5APdKTq0s6B7WTsH6Xmx0Knj86s6Biz56mErE=
|
||||
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
|
||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
|
|
|
@ -32,13 +32,14 @@ func getUserConnection(w http.ResponseWriter, r *http.Request) (*Connection, err
|
|||
return nil, err
|
||||
}
|
||||
connID := xid.New().String()
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID)
|
||||
protocol := getProtocolFromRequest(r)
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, connID)
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return nil, err
|
||||
}
|
||||
connection := &Connection{
|
||||
BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, util.GetHTTPLocalAddress(r),
|
||||
BaseConnection: common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r),
|
||||
r.RemoteAddr, user),
|
||||
request: r,
|
||||
}
|
||||
|
@ -552,7 +553,7 @@ func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirm
|
|||
return errors.New("invalid token claims")
|
||||
}
|
||||
user, err := dataprovider.CheckUserAndPass(claims.Username, currentPassword, util.GetIPFromRemoteAddress(r.RemoteAddr),
|
||||
common.ProtocolHTTP)
|
||||
getProtocolFromRequest(r))
|
||||
if err != nil {
|
||||
return util.NewValidationError("current password does not match")
|
||||
}
|
||||
|
|
|
@ -477,18 +477,25 @@ func parseRangeRequest(bytesRange string, size int64) (int64, int64, error) {
|
|||
return start, size, err
|
||||
}
|
||||
|
||||
func updateLoginMetrics(user *dataprovider.User, ip string, err error) {
|
||||
metric.AddLoginAttempt(dataprovider.LoginMethodPassword)
|
||||
func updateLoginMetrics(user *dataprovider.User, loginMethod, ip string, err error) {
|
||||
metric.AddLoginAttempt(loginMethod)
|
||||
var protocol string
|
||||
switch loginMethod {
|
||||
case dataprovider.LoginMethodIDP:
|
||||
protocol = common.ProtocolOIDC
|
||||
default:
|
||||
protocol = common.ProtocolHTTP
|
||||
}
|
||||
if err != nil && err != common.ErrInternalFailure && err != common.ErrNoCredentials {
|
||||
logger.ConnectionFailedLog(user.Username, ip, dataprovider.LoginMethodPassword, common.ProtocolHTTP, err.Error())
|
||||
logger.ConnectionFailedLog(user.Username, ip, loginMethod, protocol, err.Error())
|
||||
event := common.HostEventLoginFailed
|
||||
if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||
event = common.HostEventUserNotFound
|
||||
}
|
||||
common.AddDefenderEvent(ip, event)
|
||||
}
|
||||
metric.AddLoginResult(dataprovider.LoginMethodPassword, err)
|
||||
dataprovider.ExecutePostLoginHook(user, dataprovider.LoginMethodPassword, ip, common.ProtocolHTTP, err)
|
||||
metric.AddLoginResult(loginMethod, err)
|
||||
dataprovider.ExecutePostLoginHook(user, loginMethod, ip, protocol, err)
|
||||
}
|
||||
|
||||
func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID string) error {
|
||||
|
@ -496,7 +503,7 @@ func checkHTTPClientUser(user *dataprovider.User, r *http.Request, connectionID
|
|||
logger.Info(logSender, connectionID, "cannot login user %#v, protocol HTTP is not allowed", user.Username)
|
||||
return fmt.Errorf("protocol HTTP is not allowed for user %#v", user.Username)
|
||||
}
|
||||
if !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, nil) {
|
||||
if !isLoggedInWithOIDC(r) && !user.IsLoginMethodAllowed(dataprovider.LoginMethodPassword, nil) {
|
||||
logger.Info(logSender, connectionID, "cannot login user %#v, password login method is not allowed", user.Username)
|
||||
return fmt.Errorf("login method password is not allowed for user %#v", user.Username)
|
||||
}
|
||||
|
@ -635,3 +642,10 @@ func isUserAllowedToResetPassword(r *http.Request, user *dataprovider.User) bool
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getProtocolFromRequest(r *http.Request) string {
|
||||
if isLoggedInWithOIDC(r) {
|
||||
return common.ProtocolOIDC
|
||||
}
|
||||
return common.ProtocolHTTP
|
||||
}
|
||||
|
|
|
@ -376,31 +376,34 @@ func authenticateAdminWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTA
|
|||
|
||||
func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAuth, r *http.Request) error {
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
protocol := common.ProtocolHTTP
|
||||
if username == "" {
|
||||
err := errors.New("the provided key is not associated with any user and no username was provided")
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, err)
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
return err
|
||||
}
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolHTTP); err != nil {
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {
|
||||
return err
|
||||
}
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if err != nil {
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, err)
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
return err
|
||||
}
|
||||
if !user.Filters.AllowAPIKeyAuth {
|
||||
err := fmt.Errorf("API key authentication disabled for user %#v", user.Username)
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
return err
|
||||
}
|
||||
if err := user.CheckLoginConditions(); err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
return err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, xid.New().String())
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, xid.New().String())
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
return err
|
||||
}
|
||||
lastLogin := util.GetTimeFromMsecSinceEpoch(user.LastLogin)
|
||||
|
@ -409,7 +412,7 @@ func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAu
|
|||
defer user.CloseFs() //nolint:errcheck
|
||||
err = user.CheckFsRoot(connectionID)
|
||||
if err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
|
||||
return common.ErrInternalFailure
|
||||
}
|
||||
}
|
||||
|
@ -422,12 +425,12 @@ func authenticateUserWithAPIKey(username, keyID string, tokenAuth *jwtauth.JWTAu
|
|||
|
||||
resp, err := c.createTokenResponse(tokenAuth, tokenAudienceAPIUser)
|
||||
if err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
|
||||
return err
|
||||
}
|
||||
r.Header.Set("Authorization", fmt.Sprintf("Bearer %v", resp["access_token"]))
|
||||
dataprovider.UpdateLastLogin(&user)
|
||||
updateLoginMetrics(&user, ipAddr, nil)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -278,32 +278,32 @@ func (t *oidcToken) getUser(r *http.Request) error {
|
|||
dataprovider.UpdateAdminLastLogin(&admin)
|
||||
return nil
|
||||
}
|
||||
user, err := dataprovider.UserExists(t.Username)
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
user, err := dataprovider.GetUserAfterIDPAuth(t.Username, ipAddr, common.ProtocolOIDC)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolHTTP); err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolOIDC); err != nil {
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, err)
|
||||
return fmt.Errorf("access denied by post connect hook: %w", err)
|
||||
}
|
||||
if err := user.CheckLoginConditions(); err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, err)
|
||||
return err
|
||||
}
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, xid.New().String())
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolOIDC, xid.New().String())
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, err)
|
||||
return err
|
||||
}
|
||||
defer user.CloseFs() //nolint:errcheck
|
||||
err = user.CheckFsRoot(connectionID)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
|
||||
updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, common.ErrInternalFailure)
|
||||
return err
|
||||
}
|
||||
updateLoginMetrics(&user, ipAddr, nil)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodIDP, ipAddr, nil)
|
||||
dataprovider.UpdateLastLogin(&user)
|
||||
t.Permissions = user.Filters.WebClient
|
||||
return nil
|
||||
|
|
|
@ -2,12 +2,14 @@ package httpd
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
@ -782,23 +784,6 @@ func TestOIDCToken(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func getTestOIDCServer() *httpdServer {
|
||||
return &httpdServer{
|
||||
binding: Binding{
|
||||
OIDC: OIDC{
|
||||
ClientID: "sftpgo-client",
|
||||
ClientSecret: "jRsmE0SWnuZjP7djBqNq0mrf8QN77j2c",
|
||||
ConfigURL: fmt.Sprintf("http://%v/auth/realms/sftpgo", oidcMockAddr),
|
||||
RedirectBaseURL: "http://127.0.0.1:8081/",
|
||||
UsernameField: "preferred_username",
|
||||
RoleField: "sftpgo_role",
|
||||
},
|
||||
},
|
||||
enableWebAdmin: true,
|
||||
enableWebClient: true,
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCManager(t *testing.T) {
|
||||
require.Len(t, oidcMgr.pendingAuths, 0)
|
||||
authReq := newOIDCPendingAuth(tokenAudienceWebAdmin)
|
||||
|
@ -881,3 +866,142 @@ func TestOIDCManager(t *testing.T) {
|
|||
oidcMgr.removeToken(newToken.Cookie)
|
||||
require.Len(t, oidcMgr.tokens, 0)
|
||||
}
|
||||
|
||||
func TestOIDCPreLoginHook(t *testing.T) {
|
||||
if runtime.GOOS == osWindows {
|
||||
t.Skip("this test is not available on Windows")
|
||||
}
|
||||
username := "test_oidc_user_prelogin"
|
||||
u := dataprovider.User{
|
||||
BaseUser: sdk.BaseUser{
|
||||
Username: username,
|
||||
Password: "unused",
|
||||
HomeDir: filepath.Join(os.TempDir(), username),
|
||||
Status: 1,
|
||||
Permissions: map[string][]string{
|
||||
"/": {dataprovider.PermAny},
|
||||
},
|
||||
},
|
||||
}
|
||||
preLoginPath := filepath.Join(os.TempDir(), "prelogin.sh")
|
||||
providerConf := dataprovider.GetProviderConfig()
|
||||
err := dataprovider.Close()
|
||||
assert.NoError(t, err)
|
||||
err = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, false), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
newProviderConf := providerConf
|
||||
newProviderConf.PreLoginHook = preLoginPath
|
||||
err = dataprovider.Initialize(newProviderConf, "..", true)
|
||||
assert.NoError(t, err)
|
||||
server := getTestOIDCServer()
|
||||
err = server.binding.OIDC.initialize()
|
||||
assert.NoError(t, err)
|
||||
server.initializeRouter()
|
||||
|
||||
_, err = dataprovider.UserExists(username)
|
||||
_, ok := err.(*util.RecordNotFoundError)
|
||||
assert.True(t, ok)
|
||||
// now login with OIDC
|
||||
authReq := newOIDCPendingAuth(tokenAudienceWebClient)
|
||||
oidcMgr.addPendingAuth(authReq)
|
||||
token := &oauth2.Token{
|
||||
AccessToken: "1234",
|
||||
Expiry: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
token = token.WithExtra(map[string]interface{}{
|
||||
"id_token": "id_token_val",
|
||||
})
|
||||
server.binding.OIDC.oauth2Config = &mockOAuth2Config{
|
||||
tokenSource: &mockTokenSource{},
|
||||
authCodeURL: webOIDCRedirectPath,
|
||||
token: token,
|
||||
}
|
||||
idToken := &oidc.IDToken{
|
||||
Nonce: authReq.Nonce,
|
||||
Expiry: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
setIDTokenClaims(idToken, []byte(`{"preferred_username":"`+username+`"}`))
|
||||
server.binding.OIDC.verifier = &mockOIDCVerifier{
|
||||
err: nil,
|
||||
token: idToken,
|
||||
}
|
||||
rr := httptest.NewRecorder()
|
||||
r, err := http.NewRequest(http.MethodGet, webOIDCRedirectPath+"?state="+authReq.State, nil)
|
||||
assert.NoError(t, err)
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
assert.Equal(t, webClientFilesPath, rr.Header().Get("Location"))
|
||||
_, err = dataprovider.UserExists(username)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = dataprovider.DeleteUser(username, "", "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(preLoginPath, getPreLoginScriptContent(u, true), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
|
||||
authReq = newOIDCPendingAuth(tokenAudienceWebClient)
|
||||
oidcMgr.addPendingAuth(authReq)
|
||||
idToken = &oidc.IDToken{
|
||||
Nonce: authReq.Nonce,
|
||||
Expiry: time.Now().Add(5 * time.Minute),
|
||||
}
|
||||
setIDTokenClaims(idToken, []byte(`{"preferred_username":"`+username+`"}`))
|
||||
server.binding.OIDC.verifier = &mockOIDCVerifier{
|
||||
err: nil,
|
||||
token: idToken,
|
||||
}
|
||||
rr = httptest.NewRecorder()
|
||||
r, err = http.NewRequest(http.MethodGet, webOIDCRedirectPath+"?state="+authReq.State, nil)
|
||||
assert.NoError(t, err)
|
||||
server.router.ServeHTTP(rr, r)
|
||||
assert.Equal(t, http.StatusFound, rr.Code)
|
||||
assert.Equal(t, webClientLoginPath, rr.Header().Get("Location"))
|
||||
_, err = dataprovider.UserExists(username)
|
||||
_, ok = err.(*util.RecordNotFoundError)
|
||||
assert.True(t, ok)
|
||||
if assert.Len(t, oidcMgr.tokens, 1) {
|
||||
for k := range oidcMgr.tokens {
|
||||
oidcMgr.removeToken(k)
|
||||
}
|
||||
}
|
||||
require.Len(t, oidcMgr.pendingAuths, 0)
|
||||
require.Len(t, oidcMgr.tokens, 0)
|
||||
|
||||
err = dataprovider.Close()
|
||||
assert.NoError(t, err)
|
||||
err = dataprovider.Initialize(providerConf, "..", true)
|
||||
assert.NoError(t, err)
|
||||
err = os.Remove(preLoginPath)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func getTestOIDCServer() *httpdServer {
|
||||
return &httpdServer{
|
||||
binding: Binding{
|
||||
OIDC: OIDC{
|
||||
ClientID: "sftpgo-client",
|
||||
ClientSecret: "jRsmE0SWnuZjP7djBqNq0mrf8QN77j2c",
|
||||
ConfigURL: fmt.Sprintf("http://%v/auth/realms/sftpgo", oidcMockAddr),
|
||||
RedirectBaseURL: "http://127.0.0.1:8081/",
|
||||
UsernameField: "preferred_username",
|
||||
RoleField: "sftpgo_role",
|
||||
},
|
||||
},
|
||||
enableWebAdmin: true,
|
||||
enableWebClient: true,
|
||||
}
|
||||
}
|
||||
|
||||
func getPreLoginScriptContent(user dataprovider.User, nonJSONResponse bool) []byte {
|
||||
content := []byte("#!/bin/sh\n\n")
|
||||
if nonJSONResponse {
|
||||
content = append(content, []byte("echo 'text response'\n")...)
|
||||
return content
|
||||
}
|
||||
if len(user.Username) > 0 {
|
||||
u, _ := json.Marshal(user)
|
||||
content = append(content, []byte(fmt.Sprintf("echo '%v'\n", string(u)))...)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
|
|
@ -199,33 +199,36 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
|
|||
return
|
||||
}
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
protocol := common.ProtocolHTTP
|
||||
username := r.Form.Get("username")
|
||||
password := r.Form.Get("password")
|
||||
if username == "" || password == "" {
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, common.ErrNoCredentials)
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials)
|
||||
s.renderClientLoginPage(w, "Invalid credentials")
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, err)
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
s.renderClientLoginPage(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolHTTP); err != nil {
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {
|
||||
s.renderClientLoginPage(w, fmt.Sprintf("access denied by post connect hook: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, common.ProtocolHTTP)
|
||||
user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, protocol)
|
||||
if err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
s.renderClientLoginPage(w, dataprovider.ErrInvalidCredentials.Error())
|
||||
return
|
||||
}
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, xid.New().String())
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, xid.New().String())
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
s.renderClientLoginPage(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
@ -234,7 +237,7 @@ func (s *httpdServer) handleWebClientLoginPost(w http.ResponseWriter, r *http.Re
|
|||
err = user.CheckFsRoot(connectionID)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
|
||||
updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
|
||||
s.renderClientLoginPage(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
@ -261,7 +264,7 @@ func (s *httpdServer) handleWebClientPasswordResetPost(w http.ResponseWriter, r
|
|||
renderClientResetPwdPage(w, err.Error())
|
||||
return
|
||||
}
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, xid.New().String())
|
||||
connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
|
||||
if err := checkHTTPClientUser(user, r, connectionID); err != nil {
|
||||
renderClientResetPwdPage(w, fmt.Sprintf("Password reset successfully but unable to login: %v", err.Error()))
|
||||
return
|
||||
|
@ -325,7 +328,7 @@ func (s *httpdServer) handleWebClientTwoFactorRecoveryPost(w http.ResponseWriter
|
|||
renderClientInternalServerErrorPage(w, r, errors.New("unable to set the recovery code as used"))
|
||||
return
|
||||
}
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, xid.New().String())
|
||||
connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
|
||||
s.loginUser(w, r, &user, connectionID, util.GetIPFromRemoteAddress(r.RemoteAddr), true,
|
||||
renderClientTwoFactorRecoveryPage)
|
||||
return
|
||||
|
@ -375,7 +378,7 @@ func (s *httpdServer) handleWebClientTwoFactorPost(w http.ResponseWriter, r *htt
|
|||
renderClientTwoFactorPage(w, "Invalid authentication code")
|
||||
return
|
||||
}
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, xid.New().String())
|
||||
connectionID := fmt.Sprintf("%v_%v", getProtocolFromRequest(r), xid.New().String())
|
||||
s.loginUser(w, r, &user, connectionID, util.GetIPFromRemoteAddress(r.RemoteAddr), true, renderClientTwoFactorPage)
|
||||
}
|
||||
|
||||
|
@ -646,7 +649,7 @@ func (s *httpdServer) loginUser(
|
|||
err := c.createAndSetCookie(w, r, s.tokenAuth, audience)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, connectionID, "unable to set user login cookie %v", err)
|
||||
updateLoginMetrics(user, ipAddr, common.ErrInternalFailure)
|
||||
updateLoginMetrics(user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
|
||||
errorFunc(w, err.Error())
|
||||
return
|
||||
}
|
||||
|
@ -657,7 +660,7 @@ func (s *httpdServer) loginUser(
|
|||
http.Redirect(w, r, webClientTwoFactorPath, http.StatusFound)
|
||||
return
|
||||
}
|
||||
updateLoginMetrics(user, ipAddr, err)
|
||||
updateLoginMetrics(user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
dataprovider.UpdateLastLogin(user)
|
||||
http.Redirect(w, r, webClientFilesPath, http.StatusFound)
|
||||
}
|
||||
|
@ -708,33 +711,36 @@ func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
ipAddr := util.GetIPFromRemoteAddress(r.RemoteAddr)
|
||||
username, password, ok := r.BasicAuth()
|
||||
protocol := common.ProtocolHTTP
|
||||
if !ok {
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, common.ErrNoCredentials)
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials)
|
||||
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
|
||||
sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if username == "" || password == "" {
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}}, ipAddr, common.ErrNoCredentials)
|
||||
updateLoginMetrics(&dataprovider.User{BaseUser: sdk.BaseUser{Username: username}},
|
||||
dataprovider.LoginMethodPassword, ipAddr, common.ErrNoCredentials)
|
||||
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
|
||||
sendAPIResponse(w, r, nil, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, common.ProtocolHTTP); err != nil {
|
||||
if err := common.Config.ExecutePostConnectHook(ipAddr, protocol); err != nil {
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, common.ProtocolHTTP)
|
||||
user, err := dataprovider.CheckUserAndPass(username, password, ipAddr, protocol)
|
||||
if err != nil {
|
||||
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
sendAPIResponse(w, r, dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized),
|
||||
http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, xid.New().String())
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, xid.New().String())
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
@ -744,14 +750,14 @@ func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
|
|||
if passcode == "" {
|
||||
logger.Debug(logSender, "", "TOTP enabled for user %#v and not passcode provided, authentication refused", user.Username)
|
||||
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
|
||||
updateLoginMetrics(&user, ipAddr, dataprovider.ErrInvalidCredentials)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, dataprovider.ErrInvalidCredentials)
|
||||
sendAPIResponse(w, r, dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized),
|
||||
http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
err = user.Filters.TOTPConfig.Secret.Decrypt()
|
||||
if err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
|
||||
sendAPIResponse(w, r, fmt.Errorf("unable to decrypt TOTP secret: %w", err), http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -760,7 +766,7 @@ func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
|
|||
if !match || err != nil {
|
||||
logger.Debug(logSender, "invalid passcode for user %#v, match? %v, err: %v", user.Username, match, err)
|
||||
w.Header().Set(common.HTTPAuthenticationHeader, basicRealm)
|
||||
updateLoginMetrics(&user, ipAddr, dataprovider.ErrInvalidCredentials)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, dataprovider.ErrInvalidCredentials)
|
||||
sendAPIResponse(w, r, dataprovider.ErrInvalidCredentials, http.StatusText(http.StatusUnauthorized),
|
||||
http.StatusUnauthorized)
|
||||
return
|
||||
|
@ -771,7 +777,7 @@ func (s *httpdServer) getUserToken(w http.ResponseWriter, r *http.Request) {
|
|||
err = user.CheckFsRoot(connectionID)
|
||||
if err != nil {
|
||||
logger.Warn(logSender, connectionID, "unable to check fs root: %v", err)
|
||||
updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
@ -789,11 +795,11 @@ func (s *httpdServer) generateAndSendUserToken(w http.ResponseWriter, r *http.Re
|
|||
resp, err := c.createTokenResponse(s.tokenAuth, tokenAudienceAPIUser)
|
||||
|
||||
if err != nil {
|
||||
updateLoginMetrics(&user, ipAddr, common.ErrInternalFailure)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, common.ErrInternalFailure)
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
updateLoginMetrics(&user, ipAddr, err)
|
||||
updateLoginMetrics(&user, dataprovider.LoginMethodPassword, ipAddr, err)
|
||||
dataprovider.UpdateLastLogin(&user)
|
||||
|
||||
render.JSON(w, r, resp)
|
||||
|
|
|
@ -584,13 +584,14 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
connID := xid.New().String()
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID)
|
||||
protocol := getProtocolFromRequest(r)
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, connID)
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
renderClientForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
connection := &Connection{
|
||||
BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, util.GetHTTPLocalAddress(r),
|
||||
BaseConnection: common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r),
|
||||
r.RemoteAddr, user),
|
||||
request: r,
|
||||
}
|
||||
|
@ -727,13 +728,14 @@ func (s *httpdServer) handleClientGetDirContents(w http.ResponseWriter, r *http.
|
|||
}
|
||||
|
||||
connID := xid.New().String()
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID)
|
||||
protocol := getProtocolFromRequest(r)
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, connID)
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
connection := &Connection{
|
||||
BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, util.GetHTTPLocalAddress(r),
|
||||
BaseConnection: common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r),
|
||||
r.RemoteAddr, user),
|
||||
request: r,
|
||||
}
|
||||
|
@ -804,13 +806,14 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
|
|||
}
|
||||
|
||||
connID := xid.New().String()
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID)
|
||||
protocol := getProtocolFromRequest(r)
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, connID)
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
renderClientForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
connection := &Connection{
|
||||
BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, util.GetHTTPLocalAddress(r),
|
||||
BaseConnection: common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r),
|
||||
r.RemoteAddr, user),
|
||||
request: r,
|
||||
}
|
||||
|
@ -863,13 +866,14 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
connID := xid.New().String()
|
||||
connectionID := fmt.Sprintf("%v_%v", common.ProtocolHTTP, connID)
|
||||
protocol := getProtocolFromRequest(r)
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, connID)
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
renderClientForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
connection := &Connection{
|
||||
BaseConnection: common.NewBaseConnection(connID, common.ProtocolHTTP, util.GetHTTPLocalAddress(r),
|
||||
BaseConnection: common.NewBaseConnection(connID, protocol, util.GetHTTPLocalAddress(r),
|
||||
r.RemoteAddr, user),
|
||||
request: r,
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ const (
|
|||
loginMethodKeyAndKeyboardInt = "publickey+keyboard-interactive"
|
||||
loginMethodTLSCertificate = "TLSCertificate"
|
||||
loginMethodTLSCertificateAndPwd = "TLSCertificate+password"
|
||||
loginMethodIDP = "IDP"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -259,6 +260,27 @@ var (
|
|||
Help: "The total number of failed logins using public key + keyboard interactive",
|
||||
})
|
||||
|
||||
// totalIDPLoginAttempts is the metric that reports the total number of
|
||||
// login attempts using identity providers
|
||||
totalIDPLoginAttempts = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "sftpgo_idp_login_attempts_total",
|
||||
Help: "The total number of login attempts using Identity Providers",
|
||||
})
|
||||
|
||||
// totalIDPLoginOK is the metric that reports the total number of
|
||||
// successful logins using identity providers
|
||||
totalIDPLoginOK = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "sftpgo_idp_login_ok_total",
|
||||
Help: "The total number of successful logins using Identity Providers",
|
||||
})
|
||||
|
||||
// totalIDPLoginFailed is the metric that reports the total number of
|
||||
// failed logins using identity providers
|
||||
totalIDPLoginFailed = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "sftpgo_idp_login_ko_total",
|
||||
Help: "The total number of failed logins using Identity Providers",
|
||||
})
|
||||
|
||||
totalHTTPRequests = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "sftpgo_http_req_total",
|
||||
Help: "The total number of HTTP requests served",
|
||||
|
@ -582,7 +604,6 @@ func TransferCompleted(bytesSent, bytesReceived int64, transferKind int, err err
|
|||
} else {
|
||||
totalUploadErrors.Inc()
|
||||
}
|
||||
totalUploadSize.Add(float64(bytesReceived))
|
||||
} else {
|
||||
// download
|
||||
if err == nil {
|
||||
|
@ -590,6 +611,11 @@ func TransferCompleted(bytesSent, bytesReceived int64, transferKind int, err err
|
|||
} else {
|
||||
totalDownloadErrors.Inc()
|
||||
}
|
||||
}
|
||||
if bytesReceived > 0 {
|
||||
totalUploadSize.Add(float64(bytesReceived))
|
||||
}
|
||||
if bytesSent > 0 {
|
||||
totalDownloadSize.Add(float64(bytesSent))
|
||||
}
|
||||
}
|
||||
|
@ -826,6 +852,8 @@ func AddLoginAttempt(authMethod string) {
|
|||
totalTLSCertLoginAttempts.Inc()
|
||||
case loginMethodTLSCertificateAndPwd:
|
||||
totalTLSCertAndPwdLoginAttempts.Inc()
|
||||
case loginMethodIDP:
|
||||
totalIDPLoginAttempts.Inc()
|
||||
default:
|
||||
totalPasswordLoginAttempts.Inc()
|
||||
}
|
||||
|
@ -846,6 +874,8 @@ func incLoginOK(authMethod string) {
|
|||
totalTLSCertLoginOK.Inc()
|
||||
case loginMethodTLSCertificateAndPwd:
|
||||
totalTLSCertAndPwdLoginOK.Inc()
|
||||
case loginMethodIDP:
|
||||
totalIDPLoginOK.Inc()
|
||||
default:
|
||||
totalPasswordLoginOK.Inc()
|
||||
}
|
||||
|
@ -866,6 +896,8 @@ func incLoginFailed(authMethod string) {
|
|||
totalTLSCertLoginFailed.Inc()
|
||||
case loginMethodTLSCertificateAndPwd:
|
||||
totalTLSCertAndPwdLoginFailed.Inc()
|
||||
case loginMethodIDP:
|
||||
totalIDPLoginFailed.Inc()
|
||||
default:
|
||||
totalPasswordLoginFailed.Inc()
|
||||
}
|
||||
|
|
|
@ -4356,6 +4356,7 @@ components:
|
|||
- DAV
|
||||
- HTTP
|
||||
- DataRetention
|
||||
- OIDC
|
||||
description: |
|
||||
Protocols:
|
||||
* `SSH` - SSH commands
|
||||
|
@ -4364,6 +4365,7 @@ components:
|
|||
* `DAV` - WebDAV
|
||||
* `HTTP` - WebClient/REST API
|
||||
* `DataRetention` - the event is generated by a data retention check
|
||||
* `OIDC` - OpenID Connect
|
||||
WebClientOptions:
|
||||
type: string
|
||||
enum:
|
||||
|
|
Loading…
Reference in a new issue