diff --git a/docs/irods.md b/docs/irods.md index 6a36b0fa2..394f32516 100644 --- a/docs/irods.md +++ b/docs/irods.md @@ -2,8 +2,6 @@ To connect SFTPGo to iRODS, you need to specify credentials and a `collection path`. For example, if your collection `some_collection` is under your account's home directory `/home/irods_user` in a zone `example_zone`, you have to set the `/example_zone/home/irods_user/some_collection`. If you want to specify a particular iRODS resource server to access, use `resource server`. You can set an empty string to `resource server` to use default resource server. An endpoint is host and port of an iRODS's catalog provider (also known as iCAT server). For example, `data.cyverse.org:1247` is the endpoint if you are connecting to [CyVerse Data Store](https://data.cyverse.org). Port can be omitted if the port is 1247. -Currently, we only support password authentication. SSL/PAM authentication is not supported. - Some SFTP commands don't work over iRODS: - `chown` and `chmod` will fail. If you want to silently ignore these method set `setstat_mode` to `1` or `2` in your configuration file diff --git a/go.mod b/go.mod index ed5d605cb..bf976db4c 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.18.8 github.com/cockroachdb/cockroach-go/v2 v2.3.3 github.com/coreos/go-oidc/v3 v3.5.0 - github.com/cyverse/go-irodsclient v0.11.4 + github.com/cyverse/go-irodsclient v0.12.8 github.com/drakkan/webdav v0.0.0-20230227175313-32996838bcd8 github.com/eikenb/pipeat v0.0.0-20210730190139-06b3e6902001 github.com/fclairamb/ftpserverlib v0.21.0 @@ -71,7 +71,7 @@ require ( golang.org/x/crypto v0.7.0 golang.org/x/net v0.9.0 golang.org/x/oauth2 v0.7.0 - golang.org/x/sys v0.7.0 + golang.org/x/sys v0.8.0 golang.org/x/time v0.3.0 google.golang.org/api v0.116.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 @@ -174,6 +174,6 @@ require ( replace ( github.com/jlaffaye/ftp => github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 github.com/robfig/cron/v3 => github.com/drakkan/cron/v3 v3.0.0-20230222140221-217a1e4d96c0 - github.com/sftpgo/sdk => github.com/cyverse/sftpgo-sdk v0.1.3-0.20230410232438-7190b52214b0 + github.com/sftpgo/sdk => github.com/cyverse/sftpgo-sdk v0.1.3-0.20230906214213-bdb8dbbe543f golang.org/x/crypto => github.com/drakkan/crypto v0.0.0-20230106095953-5417b4dfde62 ) diff --git a/go.sum b/go.sum index 43424b7d4..5a1c092af 100644 --- a/go.sum +++ b/go.sum @@ -830,10 +830,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/cyverse/go-irodsclient v0.11.4 h1:1jrF2sqvC3rb0pe4Y7koq8wOYNuHcbzmQ+BSxg7D91A= -github.com/cyverse/go-irodsclient v0.11.4/go.mod h1:Qs1cjnDN1RaBaUcaZCsRGPFqCffg/cExSBIm466nvTw= -github.com/cyverse/sftpgo-sdk v0.1.3-0.20230410232438-7190b52214b0 h1:t08LiL1z6ND1pBBfXS35HufxUJl0W+oT4DZZxFJaCQY= -github.com/cyverse/sftpgo-sdk v0.1.3-0.20230410232438-7190b52214b0/go.mod h1:Giy5vj7Gmju0nGlmBNd28DwPo0G0o1nr9XkE+vu3i+o= +github.com/cyverse/go-irodsclient v0.12.8 h1:sUaNCQ7nDxUiD+HI/hBSnUiY11P8Ph+IujJt/M8Eh48= +github.com/cyverse/go-irodsclient v0.12.8/go.mod h1:SOMr0JtAmbtYp06ZdYhxBYi47GYpV9ImW7sqKVypQhU= +github.com/cyverse/sftpgo-sdk v0.1.3-0.20230906214213-bdb8dbbe543f h1:479l82wotcLnomdIiaypZ6yUNRQ34BGf+mUiM5xVT8Q= +github.com/cyverse/sftpgo-sdk v0.1.3-0.20230906214213-bdb8dbbe543f/go.mod h1:Giy5vj7Gmju0nGlmBNd28DwPo0G0o1nr9XkE+vu3i+o= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= @@ -2462,8 +2462,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/internal/cmd/portable.go b/internal/cmd/portable.go index 96c207561..c7fcc9c7e 100644 --- a/internal/cmd/portable.go +++ b/internal/cmd/portable.go @@ -102,6 +102,12 @@ var ( portableIRODSUsername string portableIRODSProxyUsername string portableIRODSResourceServer string + portableIRODSAuthScheme string + portableIRODSSSLCACertificatePath string + portableIRODSSSLKeySize int + portableIRODSSSLAlgorithm string + portableIRODSSSLSaltSize int + protableIRODSSSLHashRounds int portableIRODSPassword string portableCmd = &cobra.Command{ Use: "portable", @@ -263,11 +269,17 @@ Please take a look at the usage below to customize the serving parameters`, }, IRODSConfig: vfs.IRODSFsConfig{ BaseIRODSFsConfig: sdk.BaseIRODSFsConfig{ - Endpoint: portableIRODSEndpoint, - CollectionPath: portableIRODSCollectionPath, - Username: portableIRODSUsername, - ProxyUsername: portableIRODSProxyUsername, - ResourceServer: portableIRODSResourceServer, + Endpoint: portableIRODSEndpoint, + CollectionPath: portableIRODSCollectionPath, + Username: portableIRODSUsername, + ProxyUsername: portableIRODSProxyUsername, + ResourceServer: portableIRODSResourceServer, + AuthScheme: portableIRODSAuthScheme, + SSLCACertificatePath: portableIRODSSSLCACertificatePath, + SSLKeySize: portableIRODSSSLKeySize, + SSLAlgorithm: portableIRODSSSLAlgorithm, + SSLSaltSize: portableIRODSSSLSaltSize, + SSLHashRounds: protableIRODSSSLHashRounds, }, Password: kms.NewPlainSecret(portableIRODSPassword), }, @@ -435,6 +447,12 @@ by overlapping round-trip times`) portableCmd.Flags().StringVar(&portableIRODSUsername, "irods-username", "", `iRODS user for iRODS provider`) portableCmd.Flags().StringVar(&portableIRODSProxyUsername, "irods-proxyusername", "", `iRODS proxy user for iRODS provider`) portableCmd.Flags().StringVar(&portableIRODSResourceServer, "irods-resource", "", `iRODS resource server for iRODS provider`) + portableCmd.Flags().StringVar(&portableIRODSAuthScheme, "irods-auth-scheme", "", `iRODS authentication scheme for iRODS provider`) + portableCmd.Flags().StringVar(&portableIRODSSSLCACertificatePath, "irods-ssl-ca-cert", "", `iRODS SSL CA Certificate file path for iRODS provider`) + portableCmd.Flags().StringVar(&portableIRODSSSLAlgorithm, "irods-ssl-algorithm", "", `iRODS SSL encryption algorithm for iRODS provider`) + portableCmd.Flags().IntVar(&portableIRODSSSLKeySize, "irods-ssl-key-size", 0, `iRODS SSL encryption key size for iRODS provider`) + portableCmd.Flags().IntVar(&portableIRODSSSLSaltSize, "irods-ssl-salt-size", 0, `iRODS SSL encryption salt size for iRODS provider`) + portableCmd.Flags().IntVar(&protableIRODSSSLHashRounds, "irods-ssl-hash-rounds", 0, `iRODS SSL encryption hash rounds for iRODS provider`) portableCmd.Flags().StringVar(&portableIRODSPassword, "irods-password", "", `iRODS password for iRODS provider`) rootCmd.AddCommand(portableCmd) } diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index 65a4d2f90..3c776fef7 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -1514,6 +1514,24 @@ func getIRODSConfig(r *http.Request) (vfs.IRODSFsConfig, error) { config.ProxyUsername = r.Form.Get("irods_proxyusername") config.CollectionPath = r.Form.Get("irods_collection") config.ResourceServer = r.Form.Get("irods_resource") + config.AuthScheme = r.Form.Get("irods_auth_scheme") + config.SSLCACertificatePath = r.Form.Get("irods_ssl_ca_cert_path") + encryptionKeySize, err := strconv.ParseInt(r.Form.Get("irods_ssl_key_size"), 10, 32) + if err != nil { + return config, fmt.Errorf("invalid irods ssl key size: %w", err) + } + config.SSLKeySize = int(encryptionKeySize) + config.SSLAlgorithm = r.Form.Get("irods_ssl_algorithm") + saltSize, err := strconv.ParseInt(r.Form.Get("irods_ssl_salt_size"), 10, 32) + if err != nil { + return config, fmt.Errorf("invalid irods ssl salt size: %w", err) + } + config.SSLSaltSize = int(saltSize) + hashRounds, err := strconv.ParseInt(r.Form.Get("irods_ssl_hash_rounds"), 10, 32) + if err != nil { + return config, fmt.Errorf("invalid irods ssl hash rounds: %w", err) + } + config.SSLHashRounds = int(hashRounds) config.Password = getSecretFromFormField(r, "irods_password") return config, err } diff --git a/internal/httpdtest/httpdtest.go b/internal/httpdtest/httpdtest.go index 4ccd06797..b8ab24134 100644 --- a/internal/httpdtest/httpdtest.go +++ b/internal/httpdtest/httpdtest.go @@ -2013,6 +2013,24 @@ func compareIRODSFsConfig(expected *vfs.Filesystem, actual *vfs.Filesystem) erro if expected.IRODSConfig.ResourceServer != actual.IRODSConfig.ResourceServer { return errors.New("IRODSFs resource server mismatch") } + if expected.IRODSConfig.AuthScheme != actual.IRODSConfig.AuthScheme { + return errors.New("IRODSFs auth scheme mismatch") + } + if expected.IRODSConfig.SSLCACertificatePath != actual.IRODSConfig.SSLCACertificatePath { + return errors.New("IRODSFs SSL CA certificate path scheme mismatch") + } + if expected.IRODSConfig.SSLKeySize != actual.IRODSConfig.SSLKeySize { + return errors.New("IRODSFs SSL encryption key size mismatch") + } + if expected.IRODSConfig.SSLAlgorithm != actual.IRODSConfig.SSLAlgorithm { + return errors.New("IRODSFs SSL encryption algorithm mismatch") + } + if expected.IRODSConfig.SSLSaltSize != actual.IRODSConfig.SSLSaltSize { + return errors.New("IRODSFs SSL salt size mismatch") + } + if expected.IRODSConfig.SSLHashRounds != actual.IRODSConfig.SSLHashRounds { + return errors.New("IRODSFs SSL hash rounds mismatch") + } if err := checkEncryptedSecret(expected.IRODSConfig.Password, actual.IRODSConfig.Password); err != nil { return fmt.Errorf("IRODSFs password mismatch: %v", err) } diff --git a/internal/vfs/filesystem.go b/internal/vfs/filesystem.go index a5cdf1606..fd4bf7b1e 100644 --- a/internal/vfs/filesystem.go +++ b/internal/vfs/filesystem.go @@ -385,11 +385,17 @@ func (f *Filesystem) GetACopy() Filesystem { }, IRODSConfig: IRODSFsConfig{ BaseIRODSFsConfig: sdk.BaseIRODSFsConfig{ - Endpoint: f.IRODSConfig.Endpoint, - CollectionPath: f.IRODSConfig.CollectionPath, - Username: f.IRODSConfig.Username, - ProxyUsername: f.IRODSConfig.ProxyUsername, - ResourceServer: f.IRODSConfig.ResourceServer, + Endpoint: f.IRODSConfig.Endpoint, + CollectionPath: f.IRODSConfig.CollectionPath, + Username: f.IRODSConfig.Username, + ProxyUsername: f.IRODSConfig.ProxyUsername, + ResourceServer: f.IRODSConfig.ResourceServer, + AuthScheme: f.IRODSConfig.AuthScheme, + SSLCACertificatePath: f.IRODSConfig.SSLCACertificatePath, + SSLKeySize: f.IRODSConfig.SSLKeySize, + SSLAlgorithm: f.IRODSConfig.SSLAlgorithm, + SSLSaltSize: f.IRODSConfig.SSLSaltSize, + SSLHashRounds: f.IRODSConfig.SSLHashRounds, }, Password: f.IRODSConfig.Password.Clone(), }, diff --git a/internal/vfs/irodsfs.go b/internal/vfs/irodsfs.go index 6de238b5d..21f312341 100644 --- a/internal/vfs/irodsfs.go +++ b/internal/vfs/irodsfs.go @@ -69,6 +69,24 @@ func (c *IRODSFsConfig) isEqual(other *IRODSFsConfig) bool { if c.ResourceServer != other.ResourceServer { return false } + if c.AuthScheme != other.AuthScheme { + return false + } + if c.SSLCACertificatePath != other.SSLCACertificatePath { + return false + } + if c.SSLKeySize != other.SSLKeySize { + return false + } + if c.SSLAlgorithm != other.SSLAlgorithm { + return false + } + if c.SSLSaltSize != other.SSLSaltSize { + return false + } + if c.SSLHashRounds != other.SSLHashRounds { + return false + } c.setEmptyCredentialsIfNil() other.setEmptyCredentialsIfNil() return c.Password.IsEqual(other.Password) @@ -105,6 +123,26 @@ func (c *IRODSFsConfig) validate() error { if c.Username == "" { return errors.New("username cannot be empty") } + if strings.ToLower(c.AuthScheme) != "" && strings.ToLower(c.AuthScheme) != "native" && strings.ToLower(c.AuthScheme) != "pam" { + return errors.New("unknown authentication scheme") + } + if strings.ToLower(c.AuthScheme) == "pam" { + if c.SSLCACertificatePath == "" { + return errors.New("SSL CA certificate path cannot be empty when PAM authentication is used") + } + if c.SSLKeySize == 0 { + return errors.New("SSL encryption key size cannot be 0 when PAM authentication is used") + } + if c.SSLAlgorithm == "" { + return errors.New("SSL encryption algorithm cannot be 0 when PAM authentication is used") + } + if c.SSLSaltSize == 0 { + return errors.New("SSL encryption salt size cannot be 0 when PAM authentication is used") + } + if c.SSLHashRounds == 0 { + return errors.New("SSL encryption has rounds cannot be 0 when PAM authentication is used") + } + } if err := c.validateCredentials(); err != nil { return err } @@ -203,7 +241,7 @@ func NewIRODSFs(connectionID, localTempDir, mountPath string, irodsConfig IRODSF config: &irodsConfig, } - fsLog(fs, logger.LevelDebug, "creating a new iRODS Fs connID: %s, localTempDir: %s, mountPath: %s\n iRODS Host: %s, Collection Path: %s", fs.connectionID, fs.localTempDir, fs.mountPath, fs.config.Endpoint, fs.config.CollectionPath) + fsLog(fs, logger.LevelDebug, "creating a new iRODS Fs connID: %s, localTempDir: %s, mountPath: %s\n iRODS Host: %s, Auth: %s, Collection Path: %s", fs.connectionID, fs.localTempDir, fs.mountPath, fs.config.Endpoint, fs.config.AuthScheme, fs.config.CollectionPath) if err := fs.config.validate(); err != nil { return fs, err @@ -764,12 +802,31 @@ func (fs *IRODSFs) createConnection() error { fs.config.ProxyUsername = fs.config.Username } - irodsAccount, err := irodstypes.CreateIRODSProxyAccount(host, port, fs.config.Username, zone, fs.config.ProxyUsername, zone, irodstypes.AuthSchemeNative, fs.config.Password.GetPayload(), fs.config.ResourceServer) - if err != nil { - return err + var irodsAccount *irodstypes.IRODSAccount + + switch strings.ToLower(fs.config.AuthScheme) { + case "", "native": + irodsAccount, err = irodstypes.CreateIRODSProxyAccount(host, port, fs.config.Username, zone, fs.config.ProxyUsername, zone, irodstypes.AuthSchemeNative, fs.config.Password.GetPayload(), fs.config.ResourceServer) + if err != nil { + return err + } + case "pam": + irodsAccount, err = irodstypes.CreateIRODSProxyAccount(host, port, fs.config.Username, zone, fs.config.ProxyUsername, zone, irodstypes.AuthSchemePAM, fs.config.Password.GetPayload(), fs.config.ResourceServer) + if err != nil { + return err + } + + sslConf, err := irodstypes.CreateIRODSSSLConfig(fs.config.SSLCACertificatePath, fs.config.SSLKeySize, fs.config.SSLAlgorithm, fs.config.SSLSaltSize, fs.config.SSLHashRounds) + if err != nil { + return err + } + + irodsAccount.SetSSLConfiguration(sslConf) + default: + return fmt.Errorf("unknown authentication scheme %s", fs.config.AuthScheme) } - fsLog(fs, logger.LevelDebug, "connecting to iRODS %s:%d", irodsAccount.Host, irodsAccount.Port) + fsLog(fs, logger.LevelDebug, "connecting to iRODS %s:%d using %s auth", irodsAccount.Host, irodsAccount.Port, irodsAccount.AuthenticationScheme) irodsClient, err := irodsfs.NewFileSystemWithDefault(irodsAccount, "sftpgo") if err != nil { diff --git a/templates/webadmin/fsconfig.html b/templates/webadmin/fsconfig.html index 5a571ad1a..fd381da70 100644 --- a/templates/webadmin/fsconfig.html +++ b/templates/webadmin/fsconfig.html @@ -562,7 +562,10 @@
+ value="{{if .IRODSConfig.Password.IsEncrypted}}{{.RedactedSecret}}{{else}}{{.IRODSConfig.Password.GetPayload}}{{end}}" aria-describedby="IRODSPasswordHelpBlock"> + + iRODS User Password. If proxy user is set, give the password for the proxy user. +
@@ -581,18 +584,83 @@
+ value="{{.IRODSConfig.ProxyUsername}}" maxlength="255" aria-describedby="IRODSProxyUsernameHelpBlock"> + + iRODS Proxy Username, optional. Proxy user must have admin privilege. +
- + iRODS Resource Server, optional.
+ +
+ +
+ + + iRODS Authentication Scheme, optional. Example: "native" or "pam". Default: "native". + +
+
+ +
+ +
+ + + iRODS SSL CA Certificate File Path, optional. Only required for PAM/SSL authentication. + +
+
+ +
+ +
+ + + iRODS SSL Encryption Algorithm, optional. Only required for PAM/SSL authentication. + +
+
+ +
+ + + iRODS SSL Encryption Key Size, optional. Only required for PAM/SSL authentication. + +
+
+ +
+ +
+ + + iRODS SSL Encryption Salt Size, optional. Only required for PAM/SSL authentication. + +
+
+ +
+ + + iRODS SSL Encryption Hash Rounds, optional. Only required for PAM/SSL authentication. + +
+
{{end}}