parent
af0c9b76c4
commit
634b723b5d
46 changed files with 1582 additions and 536 deletions
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/service"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
"github.com/drakkan/sftpgo/version"
|
||||
|
@ -143,13 +144,10 @@ Please take a look at the usage below to customize the serving parameters`,
|
|||
FsConfig: dataprovider.Filesystem{
|
||||
Provider: dataprovider.FilesystemProvider(portableFsProvider),
|
||||
S3Config: vfs.S3FsConfig{
|
||||
Bucket: portableS3Bucket,
|
||||
Region: portableS3Region,
|
||||
AccessKey: portableS3AccessKey,
|
||||
AccessSecret: vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: portableS3AccessSecret,
|
||||
},
|
||||
Bucket: portableS3Bucket,
|
||||
Region: portableS3Region,
|
||||
AccessKey: portableS3AccessKey,
|
||||
AccessSecret: kms.NewPlainSecret(portableS3AccessSecret),
|
||||
Endpoint: portableS3Endpoint,
|
||||
StorageClass: portableS3StorageClass,
|
||||
KeyPrefix: portableS3KeyPrefix,
|
||||
|
@ -157,22 +155,16 @@ Please take a look at the usage below to customize the serving parameters`,
|
|||
UploadConcurrency: portableS3ULConcurrency,
|
||||
},
|
||||
GCSConfig: vfs.GCSFsConfig{
|
||||
Bucket: portableGCSBucket,
|
||||
Credentials: vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: string(portableGCSCredentials),
|
||||
},
|
||||
Bucket: portableGCSBucket,
|
||||
Credentials: kms.NewPlainSecret(string(portableGCSCredentials)),
|
||||
AutomaticCredentials: portableGCSAutoCredentials,
|
||||
StorageClass: portableGCSStorageClass,
|
||||
KeyPrefix: portableGCSKeyPrefix,
|
||||
},
|
||||
AzBlobConfig: vfs.AzBlobFsConfig{
|
||||
Container: portableAzContainer,
|
||||
AccountName: portableAzAccountName,
|
||||
AccountKey: vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: portableAzAccountKey,
|
||||
},
|
||||
Container: portableAzContainer,
|
||||
AccountName: portableAzAccountName,
|
||||
AccountKey: kms.NewPlainSecret(portableAzAccountKey),
|
||||
Endpoint: portableAzEndpoint,
|
||||
AccessTier: portableAzAccessTier,
|
||||
SASURL: portableAzSASURL,
|
||||
|
|
|
@ -83,6 +83,11 @@ Command-line flags should be specified in the Subsystem declaration.
|
|||
}
|
||||
httpConfig := config.GetHTTPConfig()
|
||||
httpConfig.Initialize(configDir)
|
||||
kmsConfig := config.GetKMSConfig()
|
||||
if err := kmsConfig.Initialize(); err != nil {
|
||||
logger.Error(logSender, connectionID, "unable to initialize KMS: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
user, err := dataprovider.UserExists(username)
|
||||
if err == nil {
|
||||
if user.HomeDir != filepath.Clean(homedir) && !preserveHomeDir {
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/ftpd"
|
||||
"github.com/drakkan/sftpgo/httpclient"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
|
@ -43,6 +44,7 @@ type globalConfig struct {
|
|||
ProviderConf dataprovider.Config `json:"data_provider" mapstructure:"data_provider"`
|
||||
HTTPDConfig httpd.Conf `json:"httpd" mapstructure:"httpd"`
|
||||
HTTPConfig httpclient.Config `json:"http" mapstructure:"http"`
|
||||
KMSConfig kms.Configuration `json:"kms" mapstructure:"kms"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -164,6 +166,12 @@ func init() {
|
|||
CACertificates: nil,
|
||||
SkipTLSVerify: false,
|
||||
},
|
||||
KMSConfig: kms.Configuration{
|
||||
Secrets: kms.Secrets{
|
||||
URL: "",
|
||||
MasterKeyPath: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix(configEnvPrefix)
|
||||
|
@ -240,6 +248,16 @@ func GetHTTPConfig() httpclient.Config {
|
|||
return globalConf.HTTPConfig
|
||||
}
|
||||
|
||||
// GetKMSConfig returns the KMS configuration
|
||||
func GetKMSConfig() kms.Configuration {
|
||||
return globalConf.KMSConfig
|
||||
}
|
||||
|
||||
// SetKMSConfig sets the kms configuration
|
||||
func SetKMSConfig(config kms.Configuration) {
|
||||
globalConf.KMSConfig = config
|
||||
}
|
||||
|
||||
// HasServicesToStart returns true if the config defines at least a service to start.
|
||||
// Supported services are SFTP, FTP and WebDAV
|
||||
func HasServicesToStart() bool {
|
||||
|
@ -456,4 +474,6 @@ func setViperDefaults() {
|
|||
viper.SetDefault("http.timeout", globalConf.HTTPConfig.Timeout)
|
||||
viper.SetDefault("http.ca_certificates", globalConf.HTTPConfig.CACertificates)
|
||||
viper.SetDefault("http.skip_tls_verify", globalConf.HTTPConfig.SkipTLSVerify)
|
||||
viper.SetDefault("kms.secrets.url", globalConf.KMSConfig.Secrets.URL)
|
||||
viper.SetDefault("kms.secrets.master_key_path", globalConf.KMSConfig.Secrets.MasterKeyPath)
|
||||
}
|
||||
|
|
|
@ -279,6 +279,12 @@ func TestSetGetConfig(t *testing.T) {
|
|||
config.SetWebDAVDConfig(webDavConf)
|
||||
assert.Equal(t, webDavConf.CertificateFile, config.GetWebDAVDConfig().CertificateFile)
|
||||
assert.Equal(t, webDavConf.CertificateKeyFile, config.GetWebDAVDConfig().CertificateKeyFile)
|
||||
kmsConf := config.GetKMSConfig()
|
||||
kmsConf.Secrets.MasterKeyPath = "apath"
|
||||
kmsConf.Secrets.URL = "aurl"
|
||||
config.SetKMSConfig(kmsConf)
|
||||
assert.Equal(t, kmsConf.Secrets.MasterKeyPath, config.GetKMSConfig().Secrets.MasterKeyPath)
|
||||
assert.Equal(t, kmsConf.Secrets.URL, config.GetKMSConfig().Secrets.URL)
|
||||
}
|
||||
|
||||
func TestServiceToStart(t *testing.T) {
|
||||
|
@ -313,11 +319,15 @@ func TestConfigFromEnv(t *testing.T) {
|
|||
os.Setenv("SFTPGO_DATA_PROVIDER__PASSWORD_HASHING__ARGON2_OPTIONS__ITERATIONS", "41")
|
||||
os.Setenv("SFTPGO_DATA_PROVIDER__POOL_SIZE", "10")
|
||||
os.Setenv("SFTPGO_DATA_PROVIDER__ACTIONS__EXECUTE_ON", "add")
|
||||
os.Setenv("SFTPGO_KMS__SECRETS__URL", "local")
|
||||
os.Setenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH", "path")
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("SFTPGO_SFTPD__BIND_ADDRESS")
|
||||
os.Unsetenv("SFTPGO_DATA_PROVIDER__PASSWORD_HASHING__ARGON2_OPTIONS__ITERATIONS")
|
||||
os.Unsetenv("SFTPGO_DATA_PROVIDER__POOL_SIZE")
|
||||
os.Unsetenv("SFTPGO_DATA_PROVIDER__ACTIONS__EXECUTE_ON")
|
||||
os.Unsetenv("SFTPGO_KMS__SECRETS__URL")
|
||||
os.Unsetenv("SFTPGO_KMS__SECRETS__MASTER_KEY_PATH")
|
||||
})
|
||||
err := config.LoadConfig(".", "invalid config")
|
||||
assert.NoError(t, err)
|
||||
|
@ -328,4 +338,7 @@ func TestConfigFromEnv(t *testing.T) {
|
|||
assert.Equal(t, 10, dataProviderConf.PoolSize)
|
||||
assert.Len(t, dataProviderConf.Actions.ExecuteOn, 1)
|
||||
assert.Contains(t, dataProviderConf.Actions.ExecuteOn, "add")
|
||||
kmsConfig := config.GetKMSConfig()
|
||||
assert.Equal(t, "local", kmsConfig.Secrets.URL)
|
||||
assert.Equal(t, "path", kmsConfig.Secrets.MasterKeyPath)
|
||||
}
|
||||
|
|
|
@ -787,6 +787,7 @@ func joinUserAndFolders(u []byte, foldersBucket *bolt.Bucket) (User, error) {
|
|||
}
|
||||
user.VirtualFolders = folders
|
||||
}
|
||||
user.SetEmptySecretsIfNil()
|
||||
return user, err
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
|
@ -129,6 +130,7 @@ func createUserFromV4(u compatUserV4, fsConfig Filesystem) User {
|
|||
Filters: u.Filters,
|
||||
}
|
||||
user.FsConfig = fsConfig
|
||||
user.SetEmptySecretsIfNil()
|
||||
return user
|
||||
}
|
||||
|
||||
|
@ -160,12 +162,11 @@ func convertUserToV4(u User, fsConfig compatFilesystemV4) compatUserV4 {
|
|||
return user
|
||||
}
|
||||
|
||||
func getCGSCredentialsFromV4(config compatGCSFsConfigV4) (vfs.Secret, error) {
|
||||
var secret vfs.Secret
|
||||
func getCGSCredentialsFromV4(config compatGCSFsConfigV4) (*kms.Secret, error) {
|
||||
secret := kms.NewEmptySecret()
|
||||
var err error
|
||||
if len(config.Credentials) > 0 {
|
||||
secret.Status = vfs.SecretStatusPlain
|
||||
secret.Payload = string(config.Credentials)
|
||||
secret = kms.NewPlainSecret(string(config.Credentials))
|
||||
return secret, nil
|
||||
}
|
||||
if config.CredentialFile != "" {
|
||||
|
@ -173,14 +174,16 @@ func getCGSCredentialsFromV4(config compatGCSFsConfigV4) (vfs.Secret, error) {
|
|||
if err != nil {
|
||||
return secret, err
|
||||
}
|
||||
secret.Status = vfs.SecretStatusPlain
|
||||
secret.Payload = string(creds)
|
||||
secret = kms.NewPlainSecret(string(creds))
|
||||
return secret, nil
|
||||
}
|
||||
return secret, err
|
||||
}
|
||||
|
||||
func getCGSCredentialsFromV6(config vfs.GCSFsConfig, username string) (string, error) {
|
||||
if config.Credentials == nil {
|
||||
config.Credentials = kms.NewEmptySecret()
|
||||
}
|
||||
if config.Credentials.IsEmpty() {
|
||||
config.CredentialFile = filepath.Join(credentialsDirPath, fmt.Sprintf("%v_gcs_credentials.json",
|
||||
username))
|
||||
|
@ -199,7 +202,7 @@ func getCGSCredentialsFromV6(config vfs.GCSFsConfig, username string) (string, e
|
|||
return "", err
|
||||
}
|
||||
// in V4 GCS credentials were not encrypted
|
||||
return config.Credentials.Payload, nil
|
||||
return config.Credentials.GetPayload(), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
@ -229,7 +232,7 @@ func convertFsConfigToV4(fs Filesystem, username string) (compatFilesystemV4, er
|
|||
if err != nil {
|
||||
return fsV4, err
|
||||
}
|
||||
secretV4, err := utils.EncryptData(fs.S3Config.AccessSecret.Payload)
|
||||
secretV4, err := utils.EncryptData(fs.S3Config.AccessSecret.GetPayload())
|
||||
if err != nil {
|
||||
return fsV4, err
|
||||
}
|
||||
|
@ -253,7 +256,7 @@ func convertFsConfigToV4(fs Filesystem, username string) (compatFilesystemV4, er
|
|||
if err != nil {
|
||||
return fsV4, err
|
||||
}
|
||||
secretV4, err := utils.EncryptData(fs.AzBlobConfig.AccountKey.Payload)
|
||||
secretV4, err := utils.EncryptData(fs.AzBlobConfig.AccountKey.GetPayload())
|
||||
if err != nil {
|
||||
return fsV4, err
|
||||
}
|
||||
|
@ -292,14 +295,14 @@ func convertFsConfigFromV4(compatFs compatFilesystemV4, username string) (Filesy
|
|||
KeyPrefix: compatFs.S3Config.KeyPrefix,
|
||||
Region: compatFs.S3Config.Region,
|
||||
AccessKey: compatFs.S3Config.AccessKey,
|
||||
AccessSecret: vfs.Secret{},
|
||||
AccessSecret: kms.NewEmptySecret(),
|
||||
Endpoint: compatFs.S3Config.Endpoint,
|
||||
StorageClass: compatFs.S3Config.StorageClass,
|
||||
UploadPartSize: compatFs.S3Config.UploadPartSize,
|
||||
UploadConcurrency: compatFs.S3Config.UploadConcurrency,
|
||||
}
|
||||
if compatFs.S3Config.AccessSecret != "" {
|
||||
secret, err := vfs.GetSecretFromCompatString(compatFs.S3Config.AccessSecret)
|
||||
secret, err := kms.GetSecretFromCompatString(compatFs.S3Config.AccessSecret)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to convert v4 filesystem for user %#v: %v", username, err)
|
||||
return fsConfig, err
|
||||
|
@ -310,7 +313,7 @@ func convertFsConfigFromV4(compatFs compatFilesystemV4, username string) (Filesy
|
|||
fsConfig.AzBlobConfig = vfs.AzBlobFsConfig{
|
||||
Container: compatFs.AzBlobConfig.Container,
|
||||
AccountName: compatFs.AzBlobConfig.AccountName,
|
||||
AccountKey: vfs.Secret{},
|
||||
AccountKey: kms.NewEmptySecret(),
|
||||
Endpoint: compatFs.AzBlobConfig.Endpoint,
|
||||
SASURL: compatFs.AzBlobConfig.SASURL,
|
||||
KeyPrefix: compatFs.AzBlobConfig.KeyPrefix,
|
||||
|
@ -320,7 +323,7 @@ func convertFsConfigFromV4(compatFs compatFilesystemV4, username string) (Filesy
|
|||
AccessTier: compatFs.AzBlobConfig.AccessTier,
|
||||
}
|
||||
if compatFs.AzBlobConfig.AccountKey != "" {
|
||||
secret, err := vfs.GetSecretFromCompatString(compatFs.AzBlobConfig.AccountKey)
|
||||
secret, err := kms.GetSecretFromCompatString(compatFs.AzBlobConfig.AccountKey)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelError, "unable to convert v4 filesystem for user %#v: %v", username, err)
|
||||
return fsConfig, err
|
||||
|
|
|
@ -42,6 +42,7 @@ import (
|
|||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/drakkan/sftpgo/httpclient"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/metrics"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
|
@ -124,6 +125,7 @@ var (
|
|||
sqlTableFoldersMapping = "folders_mapping"
|
||||
sqlTableSchemaVersion = "schema_version"
|
||||
argon2Params *argon2id.Params
|
||||
lastLoginMinDelay = 10 * time.Minute
|
||||
)
|
||||
|
||||
type schemaVersion struct {
|
||||
|
@ -577,7 +579,12 @@ func UpdateLastLogin(user User) error {
|
|||
if config.ManageUsers == 0 {
|
||||
return &MethodDisabledError{err: manageUsersDisabledError}
|
||||
}
|
||||
return provider.updateLastLogin(user.Username)
|
||||
lastLogin := utils.GetTimeFromMsecSinceEpoch(user.LastLogin)
|
||||
diff := -time.Until(lastLogin)
|
||||
if diff < 0 || diff > lastLoginMinDelay {
|
||||
return provider.updateLastLogin(user.Username)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserQuota updates the quota for the given SFTP user adding filesAdd and sizeAdd.
|
||||
|
@ -1099,12 +1106,12 @@ func saveGCSCredentials(user *User) error {
|
|||
if user.FsConfig.Provider != GCSFilesystemProvider {
|
||||
return nil
|
||||
}
|
||||
if user.FsConfig.GCSConfig.Credentials.Payload == "" {
|
||||
if user.FsConfig.GCSConfig.Credentials.GetPayload() == "" {
|
||||
return nil
|
||||
}
|
||||
if config.PreferDatabaseCredentials {
|
||||
if user.FsConfig.GCSConfig.Credentials.IsPlain() {
|
||||
user.FsConfig.GCSConfig.Credentials.AdditionalData = user.Username
|
||||
user.FsConfig.GCSConfig.Credentials.SetAdditionalData(user.Username)
|
||||
err := user.FsConfig.GCSConfig.Credentials.Encrypt()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -1113,7 +1120,7 @@ func saveGCSCredentials(user *User) error {
|
|||
return nil
|
||||
}
|
||||
if user.FsConfig.GCSConfig.Credentials.IsPlain() {
|
||||
user.FsConfig.GCSConfig.Credentials.AdditionalData = user.Username
|
||||
user.FsConfig.GCSConfig.Credentials.SetAdditionalData(user.Username)
|
||||
err := user.FsConfig.GCSConfig.Credentials.Encrypt()
|
||||
if err != nil {
|
||||
return &ValidationError{err: fmt.Sprintf("could not encrypt GCS credentials: %v", err)}
|
||||
|
@ -1132,7 +1139,7 @@ func saveGCSCredentials(user *User) error {
|
|||
if err != nil {
|
||||
return &ValidationError{err: fmt.Sprintf("could not save GCS credentials: %v", err)}
|
||||
}
|
||||
user.FsConfig.GCSConfig.Credentials = vfs.Secret{}
|
||||
user.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1143,7 +1150,7 @@ func validateFilesystemConfig(user *User) error {
|
|||
return &ValidationError{err: fmt.Sprintf("could not validate s3config: %v", err)}
|
||||
}
|
||||
if user.FsConfig.S3Config.AccessSecret.IsPlain() {
|
||||
user.FsConfig.S3Config.AccessSecret.AdditionalData = user.Username
|
||||
user.FsConfig.S3Config.AccessSecret.SetAdditionalData(user.Username)
|
||||
err = user.FsConfig.S3Config.AccessSecret.Encrypt()
|
||||
if err != nil {
|
||||
return &ValidationError{err: fmt.Sprintf("could not encrypt s3 access secret: %v", err)}
|
||||
|
@ -1166,7 +1173,7 @@ func validateFilesystemConfig(user *User) error {
|
|||
return &ValidationError{err: fmt.Sprintf("could not validate Azure Blob config: %v", err)}
|
||||
}
|
||||
if user.FsConfig.AzBlobConfig.AccountKey.IsPlain() {
|
||||
user.FsConfig.AzBlobConfig.AccountKey.AdditionalData = user.Username
|
||||
user.FsConfig.AzBlobConfig.AccountKey.SetAdditionalData(user.Username)
|
||||
err = user.FsConfig.AzBlobConfig.AccountKey.Encrypt()
|
||||
if err != nil {
|
||||
return &ValidationError{err: fmt.Sprintf("could not encrypt Azure blob account key: %v", err)}
|
||||
|
@ -1220,6 +1227,7 @@ func validateFolder(folder *vfs.BaseVirtualFolder) error {
|
|||
}
|
||||
|
||||
func validateUser(user *User) error {
|
||||
user.SetEmptySecretsIfNil()
|
||||
buildUserHomeDir(user)
|
||||
if err := validateBaseParams(user); err != nil {
|
||||
return err
|
||||
|
@ -2131,7 +2139,7 @@ func CacheWebDAVUser(cachedUser *CachedUser, maxSize int) {
|
|||
}
|
||||
}
|
||||
|
||||
if len(cachedUser.User.Username) > 0 {
|
||||
if cachedUser.User.Username != "" {
|
||||
webDAVUsersCache.Store(cachedUser.User.Username, cachedUser)
|
||||
}
|
||||
}
|
||||
|
@ -2143,7 +2151,7 @@ func GetCachedWebDAVUser(username string) (interface{}, bool) {
|
|||
|
||||
// RemoveCachedWebDAVUser removes a cached WebDAV user
|
||||
func RemoveCachedWebDAVUser(username string) {
|
||||
if len(username) > 0 {
|
||||
if username != "" {
|
||||
webDAVUsersCache.Delete(username)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -264,7 +264,8 @@ func (p MemoryProvider) dumpUsers() ([]User, error) {
|
|||
return users, errMemoryProviderClosed
|
||||
}
|
||||
for _, username := range p.dbHandle.usernames {
|
||||
user := p.dbHandle.users[username]
|
||||
u := p.dbHandle.users[username]
|
||||
user := u.getACopy()
|
||||
err = addCredentialsToUser(&user)
|
||||
if err != nil {
|
||||
return users, err
|
||||
|
@ -315,7 +316,8 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s
|
|||
if itNum <= offset {
|
||||
continue
|
||||
}
|
||||
user := p.dbHandle.users[username]
|
||||
u := p.dbHandle.users[username]
|
||||
user := u.getACopy()
|
||||
user.HideConfidentialData()
|
||||
users = append(users, user)
|
||||
if len(users) >= limit {
|
||||
|
@ -329,7 +331,8 @@ func (p MemoryProvider) getUsers(limit int, offset int, order string, username s
|
|||
continue
|
||||
}
|
||||
username := p.dbHandle.usernames[i]
|
||||
user := p.dbHandle.users[username]
|
||||
u := p.dbHandle.users[username]
|
||||
user := u.getACopy()
|
||||
user.HideConfidentialData()
|
||||
users = append(users, user)
|
||||
if len(users) >= limit {
|
||||
|
|
|
@ -59,7 +59,12 @@ func initializeMySQLProvider() error {
|
|||
providerLog(logger.LevelDebug, "mysql database handle created, connection string: %#v, pool size: %v",
|
||||
getMySQLConnectionString(true), config.PoolSize)
|
||||
dbHandle.SetMaxOpenConns(config.PoolSize)
|
||||
dbHandle.SetConnMaxLifetime(1800 * time.Second)
|
||||
if config.PoolSize > 0 {
|
||||
dbHandle.SetMaxIdleConns(config.PoolSize)
|
||||
} else {
|
||||
dbHandle.SetMaxIdleConns(2)
|
||||
}
|
||||
dbHandle.SetConnMaxLifetime(240 * time.Second)
|
||||
provider = MySQLProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
providerLog(logger.LevelWarn, "error creating mysql database handler, connection string: %#v, error: %v",
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
// we import lib/pq here to be able to disable PostgreSQL support using a build tag
|
||||
_ "github.com/lib/pq"
|
||||
|
@ -58,6 +59,12 @@ func initializePGSQLProvider() error {
|
|||
providerLog(logger.LevelDebug, "postgres database handle created, connection string: %#v, pool size: %v",
|
||||
getPGSQLConnectionString(true), config.PoolSize)
|
||||
dbHandle.SetMaxOpenConns(config.PoolSize)
|
||||
if config.PoolSize > 0 {
|
||||
dbHandle.SetMaxIdleConns(config.PoolSize)
|
||||
} else {
|
||||
dbHandle.SetMaxIdleConns(2)
|
||||
}
|
||||
dbHandle.SetConnMaxLifetime(240 * time.Second)
|
||||
provider = PGSQLProvider{dbHandle: dbHandle}
|
||||
} else {
|
||||
providerLog(logger.LevelWarn, "error creating postgres database handler, connection string: %#v, error: %v",
|
||||
|
|
|
@ -446,6 +446,7 @@ func getUserFromDbRow(row *sql.Row, rows *sql.Rows) (User, error) {
|
|||
if additionalInfo.Valid {
|
||||
user.AdditionalInfo = additionalInfo.String
|
||||
}
|
||||
user.SetEmptySecretsIfNil()
|
||||
return user, err
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"golang.org/x/net/webdav"
|
||||
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
|
@ -768,7 +769,21 @@ func (u User) GetDeniedIPAsString() string {
|
|||
return result
|
||||
}
|
||||
|
||||
// SetEmptySecretsIfNil sets the secrets to empty if nil
|
||||
func (u *User) SetEmptySecretsIfNil() {
|
||||
if u.FsConfig.S3Config.AccessSecret == nil {
|
||||
u.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
}
|
||||
if u.FsConfig.GCSConfig.Credentials == nil {
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
|
||||
}
|
||||
if u.FsConfig.AzBlobConfig.AccountKey == nil {
|
||||
u.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) getACopy() User {
|
||||
u.SetEmptySecretsIfNil()
|
||||
pubKeys := make([]string, len(u.PublicKeys))
|
||||
copy(pubKeys, u.PublicKeys)
|
||||
virtualFolders := make([]vfs.VirtualFolder, len(u.VirtualFolders))
|
||||
|
@ -799,7 +814,7 @@ func (u *User) getACopy() User {
|
|||
Bucket: u.FsConfig.S3Config.Bucket,
|
||||
Region: u.FsConfig.S3Config.Region,
|
||||
AccessKey: u.FsConfig.S3Config.AccessKey,
|
||||
AccessSecret: u.FsConfig.S3Config.AccessSecret,
|
||||
AccessSecret: u.FsConfig.S3Config.AccessSecret.Clone(),
|
||||
Endpoint: u.FsConfig.S3Config.Endpoint,
|
||||
StorageClass: u.FsConfig.S3Config.StorageClass,
|
||||
KeyPrefix: u.FsConfig.S3Config.KeyPrefix,
|
||||
|
@ -809,7 +824,7 @@ func (u *User) getACopy() User {
|
|||
GCSConfig: vfs.GCSFsConfig{
|
||||
Bucket: u.FsConfig.GCSConfig.Bucket,
|
||||
CredentialFile: u.FsConfig.GCSConfig.CredentialFile,
|
||||
Credentials: u.FsConfig.GCSConfig.Credentials,
|
||||
Credentials: u.FsConfig.GCSConfig.Credentials.Clone(),
|
||||
AutomaticCredentials: u.FsConfig.GCSConfig.AutomaticCredentials,
|
||||
StorageClass: u.FsConfig.GCSConfig.StorageClass,
|
||||
KeyPrefix: u.FsConfig.GCSConfig.KeyPrefix,
|
||||
|
@ -817,7 +832,7 @@ func (u *User) getACopy() User {
|
|||
AzBlobConfig: vfs.AzBlobFsConfig{
|
||||
Container: u.FsConfig.AzBlobConfig.Container,
|
||||
AccountName: u.FsConfig.AzBlobConfig.AccountName,
|
||||
AccountKey: u.FsConfig.AzBlobConfig.AccountKey,
|
||||
AccountKey: u.FsConfig.AzBlobConfig.AccountKey.Clone(),
|
||||
Endpoint: u.FsConfig.AzBlobConfig.Endpoint,
|
||||
SASURL: u.FsConfig.AzBlobConfig.SASURL,
|
||||
KeyPrefix: u.FsConfig.AzBlobConfig.KeyPrefix,
|
||||
|
|
|
@ -28,6 +28,7 @@ For each account, the following properties can be configured:
|
|||
- `chtimes` changing file or directory access and modification time is allowed
|
||||
- `upload_bandwidth` maximum upload bandwidth as KB/s, 0 means unlimited.
|
||||
- `download_bandwidth` maximum download bandwidth as KB/s, 0 means unlimited.
|
||||
- `last_login` last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes
|
||||
- `allowed_ip`, List of IP/Mask allowed to login. Any IP address not contained in this list cannot login. IP/Mask must be in CIDR notation as defined in RFC 4632 and RFC 4291, for example "192.0.2.0/24" or "2001:db8::/32"
|
||||
- `denied_ip`, List of IP/Mask not allowed to login. If an IP address is both allowed and denied then login will be denied
|
||||
- `max_upload_file_size`, max allowed size, as bytes, for a single file upload. The upload will be aborted if/when the size of the file being sent exceeds this limit. 0 means unlimited. This restriction does not apply for SSH system commands such as `git` and `rsync`
|
||||
|
@ -53,20 +54,20 @@ For each account, the following properties can be configured:
|
|||
- `s3_bucket`, required for S3 filesystem
|
||||
- `s3_region`, required for S3 filesystem. Must match the region for your bucket. You can find here the list of available [AWS regions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions). For example if your bucket is at `Frankfurt` you have to set the region to `eu-central-1`
|
||||
- `s3_access_key`
|
||||
- `s3_access_secret`, if provided it is stored encrypted (AES-256-GCM). You can leave access key and access secret blank to use credentials from environment
|
||||
- `s3_access_secret`, if provided it is stored encrypted based on kms configuration. You can leave access key and access secret blank to use credentials from environment
|
||||
- `s3_endpoint`, specifies a S3 endpoint (server) different from AWS. It is not required if you are connecting to AWS
|
||||
- `s3_storage_class`, leave blank to use the default or specify a valid AWS [storage class](https://docs.aws.amazon.com/AmazonS3/latest/dev/storage-class-intro.html)
|
||||
- `s3_key_prefix`, allows to restrict access to the folder identified by this prefix and its contents
|
||||
- `s3_upload_part_size`, the buffer size for multipart uploads (MB). Zero means the default (5 MB). Minimum is 5
|
||||
- `s3_upload_concurrency` how many parts are uploaded in parallel
|
||||
- `gcs_bucket`, required for GCS filesystem
|
||||
- `gcs_credentials`, Google Cloud Storage JSON credentials base64 encoded
|
||||
- `gcs_credentials`, Google Cloud Storage JSON credentials base64 encoded. Credentials are stored encrypted based on kms configuration
|
||||
- `gcs_automatic_credentials`, integer. Set to 1 to use Application Default Credentials strategy or set to 0 to use explicit credentials via `gcs_credentials`
|
||||
- `gcs_storage_class`
|
||||
- `gcs_key_prefix`, allows to restrict access to the folder identified by this prefix and its contents
|
||||
- `az_container`, Azure Blob Storage container
|
||||
- `az_account_name`, Azure account name. leave blank to use SAS URL
|
||||
- `az_account_key`, Azure account key. leave blank to use SAS URL. If provided it is stored encrypted (AES-256-GCM)
|
||||
- `az_account_key`, Azure account key. leave blank to use SAS URL. If provided it is stored encrypted based on kms configuration
|
||||
- `az_sas_url`, Azure shared access signature URL
|
||||
- `az_endpoint`, Default is "blob.core.windows.net". If you use the emulator the endpoint must include the protocol, for example "http://127.0.0.1:10000"
|
||||
- `az_upload_part_size`, the buffer size for multipart uploads (MB). Zero means the default (4 MB)
|
||||
|
|
|
@ -163,6 +163,10 @@ The configuration file contains the following sections:
|
|||
- `timeout`, integer. Timeout specifies a time limit, in seconds, for requests.
|
||||
- `ca_certificates`, list of strings. List of paths to extra CA certificates to trust. The paths can be absolute or relative to the config dir. Adding trusted CA certificates is a convenient way to use self-signed certificates without defeating the purpose of using TLS.
|
||||
- `skip_tls_verify`, boolean. if enabled the HTTP client accepts any TLS certificate presented by the server and any host name in that certificate. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing.
|
||||
- **kms**, configuration for the Key Management Service, more details can be found [here](./kms.md)
|
||||
- `secrets`
|
||||
- `url`
|
||||
- `master_key_path`
|
||||
|
||||
A full example showing the default config (in JSON format) can be found [here](../sftpgo.json).
|
||||
|
||||
|
|
65
docs/kms.md
Normal file
65
docs/kms.md
Normal file
|
@ -0,0 +1,65 @@
|
|||
# Key Management Services
|
||||
|
||||
SFTPGo stores sensitive data such as Cloud accounts credentials. This data are stored as ciphertext and only loaded to RAM in plaintext when needed.
|
||||
|
||||
## Supported Services for encryption and decryption
|
||||
|
||||
The `secrets` section of the `kms` configuration allows to configure how to encrypt and decrypt sensitive data. The following configuration parameters are available:
|
||||
|
||||
- `url` defines the URI to the KMS service
|
||||
- `master_key_path` defines the absolute path to a file containing the master encryption key. This could be, for example, a docker secrets or a file protected with filesystem level permissions.
|
||||
|
||||
We use [Go CDK](https://gocloud.dev/howto/secrets/) to access several key management services in a portable way.
|
||||
|
||||
### Local provider
|
||||
|
||||
If the `url` is empty SFTPGo uses local encryption for keeping secrets. Internally, it uses the [NaCl secret box](https://godoc.org/golang.org/x/crypto/nacl/secretbox) algorithm to perform encryption and authentication.
|
||||
|
||||
We first generate a random key, then the per-object encryption key is derived from this random key in the following way:
|
||||
|
||||
1. a master key is provided: the encryption key is derived using the HMAC-based Extract-and-Expand Key Derivation Function (HKDF) as defined in [RFC 5869](http://tools.ietf.org/html/rfc5869)
|
||||
2. no master key is provided: the encryption key is derived as simple hash of the random key. This is the default configuration.
|
||||
|
||||
For compatibility with SFTPGo versions 1.2.x and before we also support encryption based on `AES-256-GCM`. The data encrypted with this algorithm will never use the master key to keep backward compatibility.
|
||||
|
||||
### Google Cloud Key Management Service
|
||||
|
||||
To use keys from Google Cloud Platform’s [Key Management Service](https://cloud.google.com/kms/) (GCP KMS) you have to use `gcpkms` as URL scheme like this:
|
||||
|
||||
```shell
|
||||
gcpkms://projects/[PROJECT_ID]/locations/[LOCATION]/keyRings/[KEY_RING]/cryptoKeys/[KEY]
|
||||
```
|
||||
|
||||
SFTPGo will use Application Default Credentials. See [here](https://cloud.google.com/docs/authentication/production) for alternatives such as environment variables.
|
||||
|
||||
The URL host+path are used as the key resource ID; see [here](https://cloud.google.com/kms/docs/object-hierarchy#key) for more details.
|
||||
|
||||
If a master key is provided we first encrypt the plaintext data using the local provider and then we encrypt the resulting payload using the Cloud provider and store this ciphertext.
|
||||
|
||||
### AWS Key Management Service
|
||||
|
||||
To use customer master keys from Amazon Web Service’s [Key Management Service](https://aws.amazon.com/kms/) (AWS KMS) you have to use `awskms` as URL scheme. You can use the key’s ID, alias, or Amazon Resource Name (ARN) to identify the key. You should specify the region query parameter to ensure your application connects to the correct region.
|
||||
|
||||
Here are some examples:
|
||||
|
||||
- By ID: `awskms://1234abcd-12ab-34cd-56ef-1234567890ab?region=us-east-1`
|
||||
- By alias: `awskms://alias/ExampleAlias?region=us-east-1`
|
||||
- By ARN: `arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34bc-56ef-1234567890ab?region=us-east-1`
|
||||
|
||||
SFTPGo will use the default AWS session. See [AWS Session](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/) to learn about authentication alternatives such as environment variables.
|
||||
|
||||
If a master key is provided we first encrypt the plaintext data using the local provider and then we encrypt the resulting payload using the Cloud provider and store this ciphertext.
|
||||
|
||||
### HashiCorp Vault
|
||||
|
||||
To use the [transit secrets engine](https://www.vaultproject.io/docs/secrets/transit/index.html) in [Vault](https://www.vaultproject.io/) you have to use `hashivault` as URL scheme like this: `hashivault://mykey`.
|
||||
|
||||
The Vault server endpoint and authentication token are specified using the environment variables `VAULT_SERVER_URL` and `VAULT_SERVER_TOKEN`, respectively.
|
||||
|
||||
If a master key is provided we first encrypt the plaintext data using the local provider and then we encrypt the resulting payload using Vault and store this ciphertext.
|
||||
|
||||
### Notes
|
||||
|
||||
- The KMS configuration is global.
|
||||
- If you set a master key you will be unable to decrypt the data without this key and the SFTPGo users that need the data as plain text will be unable to login.
|
||||
- You can start using the local provider and then switch to an external one but you can't switch between external providers and still be able to decrypt the data encrypted using the previous provider.
|
|
@ -29,7 +29,7 @@ The response is something like this:
|
|||
{"message":"User created successfully.","user":{"id":xxxxxxxx},"success":true}
|
||||
```
|
||||
|
||||
Save the user id somewhere and add a reference to the matching SFTPGo account.
|
||||
Save the user id somewhere and add a reference to the matching SFTPGo account. You could also store this ID in the `additional_info` SFTPGo user field.
|
||||
|
||||
After this step you can use the Authy app installed on your phone to generate TOTP codes.
|
||||
|
||||
|
|
|
@ -60,8 +60,8 @@ Output:
|
|||
"s3config": {
|
||||
"access_key": "accesskey",
|
||||
"access_secret": {
|
||||
"payload": "ac46cec75466ba77e47f536436783b729ca5bbbb53252fda0de51f785a6da11ffb03",
|
||||
"status": "AES-256-GCM"
|
||||
"payload": "dcd07e64a5ef5ede37b978198ca396ea9aee92453208ee2fee6f25407e47bf2119ba8edf2e81f91999bd5386c1a7",
|
||||
"status": "Secretbox"
|
||||
},
|
||||
"bucket": "test",
|
||||
"endpoint": "http://127.0.0.1:9000",
|
||||
|
@ -178,9 +178,16 @@ Output:
|
|||
"download_bandwidth": 80,
|
||||
"expiration_date": 0,
|
||||
"filesystem": {
|
||||
"gcsconfig": {},
|
||||
"azblobconfig": {
|
||||
"account_key": {}
|
||||
},
|
||||
"gcsconfig": {
|
||||
"credentials": {}
|
||||
},
|
||||
"provider": 0,
|
||||
"s3config": {}
|
||||
"s3config": {
|
||||
"access_secret": {}
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"denied_ip": [
|
||||
|
@ -253,9 +260,16 @@ Output:
|
|||
"download_bandwidth": 80,
|
||||
"expiration_date": 0,
|
||||
"filesystem": {
|
||||
"gcsconfig": {},
|
||||
"azblobconfig": {
|
||||
"account_key": {}
|
||||
},
|
||||
"gcsconfig": {
|
||||
"credentials": {}
|
||||
},
|
||||
"provider": 0,
|
||||
"s3config": {}
|
||||
"s3config": {
|
||||
"access_secret": {}
|
||||
}
|
||||
},
|
||||
"filters": {
|
||||
"denied_ip": [
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/ftpd"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
)
|
||||
|
@ -131,6 +132,13 @@ func TestMain(m *testing.M) {
|
|||
httpConfig := config.GetHTTPConfig()
|
||||
httpConfig.Initialize(configDir)
|
||||
|
||||
kmsConfig := config.GetKMSConfig()
|
||||
err = kmsConfig.Initialize()
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("error initializing kms: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
httpdConf := config.GetHTTPDConfig()
|
||||
httpdConf.BindPort = 8079
|
||||
httpd.SetBaseURLAndCredentials("http://127.0.0.1:8079", "", "")
|
||||
|
@ -876,10 +884,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
u := getTestUser()
|
||||
u.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
u.FsConfig.GCSConfig.Bucket = "test"
|
||||
u.FsConfig.GCSConfig.Credentials = vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: `{ "type": "service_account" }`,
|
||||
}
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ "type": "service_account" }`)
|
||||
|
||||
providerConf := config.GetProviderConf()
|
||||
providerConf.PreferDatabaseCredentials = true
|
||||
|
@ -900,10 +905,10 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.GCSConfig.Credentials.Status)
|
||||
assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.Payload)
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.AdditionalData)
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.Key)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())
|
||||
assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload())
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetKey())
|
||||
|
||||
assert.NoFileExists(t, credentialsFile)
|
||||
|
||||
|
@ -928,10 +933,7 @@ func TestLoginInvalidFs(t *testing.T) {
|
|||
u := getTestUser()
|
||||
u.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
u.FsConfig.GCSConfig.Bucket = "test"
|
||||
u.FsConfig.GCSConfig.Credentials = vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: "invalid JSON for credentials",
|
||||
}
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
3
go.mod
3
go.mod
|
@ -23,6 +23,7 @@ require (
|
|||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.5
|
||||
github.com/miekg/dns v1.1.35 // indirect
|
||||
github.com/minio/sha256-simd v0.1.1
|
||||
github.com/mitchellh/mapstructure v1.3.3 // indirect
|
||||
github.com/otiai10/copy v1.2.0
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
|
@ -43,6 +44,8 @@ require (
|
|||
github.com/studio-b12/gowebdav v0.0.0-20200929080739-bdacfab94796
|
||||
go.etcd.io/bbolt v1.3.5
|
||||
go.uber.org/automaxprocs v1.3.0
|
||||
gocloud.dev v0.20.0
|
||||
gocloud.dev/secrets/hashivault v0.20.0
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 // indirect
|
||||
|
|
163
go.sum
163
go.sum
|
@ -1,16 +1,21 @@
|
|||
bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
|
||||
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
|
||||
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
|
||||
cloud.google.com/go v0.55.0/go.mod h1:ZHmoY+/lIMNkN2+fBmuTiqZ4inFhvQad8ft7MT8IV5Y=
|
||||
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
|
||||
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
|
||||
cloud.google.com/go v0.58.0/go.mod h1:W+9FnSUw6nhVwXlFcp1eL+krq5+HQUJeUogSeJZZiWg=
|
||||
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
|
||||
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
|
||||
cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko=
|
||||
|
@ -25,6 +30,7 @@ cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM7
|
|||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/firestore v1.2.0/go.mod h1:iISCjWnTpnoJT1R287xRdjvQHJrxQOpeah4phb5D3h0=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
|
@ -33,23 +39,61 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
|
|||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.9.0/go.mod h1:m+/etGaqZbylxaNT876QGXqEHp4PR2Rq5GMqICWb9bU=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.12.0 h1:4y3gHptW1EHVtcPAVE0eBBlFuGqEejTTG3KdIE0lUX4=
|
||||
cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho=
|
||||
contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA=
|
||||
contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e5CWqyUk/cLzKnWsOKPVW3no6OTw=
|
||||
contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
|
||||
contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-amqp-common-go/v3 v3.0.0/go.mod h1:SY08giD/XbhTz07tJdpw1SoxQXHPN30+DI3Z04SYqyg=
|
||||
github.com/Azure/azure-pipeline-go v0.2.1/go.mod h1:UGSo8XybXnIGZ3epmeBw7Jdz+HiUVpqIlpz/HKHylF4=
|
||||
github.com/Azure/azure-pipeline-go v0.2.2/go.mod h1:4rQ/NZncSvGqNkkOsNpOU1tgoNuIlp9AfUH5G1tvCHc=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
|
||||
github.com/Azure/azure-pipeline-go v0.2.3/go.mod h1:x841ezTBIMG6O3lAcl8ATHnsOPVl2bqk7S3ta6S6u4k=
|
||||
github.com/Azure/azure-sdk-for-go v37.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
|
||||
github.com/Azure/azure-service-bus-go v0.10.1/go.mod h1:E/FOceuKAFUfpbIJDKWz/May6guE+eGibfGT6q+n1to=
|
||||
github.com/Azure/azure-storage-blob-go v0.9.0/go.mod h1:8UBPbiOhrMQ4pLPi3gA1tXnpjrS76UYE/fo5A40vf4g=
|
||||
github.com/Azure/azure-storage-blob-go v0.11.0 h1:WCTHKKNkHlzm7lzUNXRSD11784LwJqdrxnwWJxsJQHg=
|
||||
github.com/Azure/azure-storage-blob-go v0.11.0/go.mod h1:A0u4VjtpgZJ7Y7um/+ix2DHBuEKFC6sEIlj0xc13a4Q=
|
||||
github.com/Azure/go-amqp v0.12.6/go.mod h1:qApuH6OFTSKZFmCOxccvAv5rLizBQf4v8pRmG138DPo=
|
||||
github.com/Azure/go-amqp v0.12.7/go.mod h1:qApuH6OFTSKZFmCOxccvAv5rLizBQf4v8pRmG138DPo=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
|
||||
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||
github.com/Azure/go-autorest/autorest v0.9.3 h1:OZEIaBbMdUE/Js+BQKlpO81XlISgipr6yDJ+PSwsgi4=
|
||||
github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.8.3/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.2 h1:Aze/GQeAN1RRbGmnUJvUj+tFGBzFdIg3293/A9rbxC4=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.2/go.mod h1:/3SMAM86bP6wC9Ev35peQDUeqFZBMH07vvUOmg4z/fE=
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.4.2/go.mod h1:90gmfKdlmKgfjUpnCEpOJzsUEjrWDSLwHIG73tSXddM=
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.3.1/go.mod h1:ZG5p860J94/0kI9mNJVoIoLgXcirM2gF5i2kWloofxw=
|
||||
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
|
||||
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
|
||||
github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA=
|
||||
github.com/Azure/go-autorest/autorest/validation v0.2.0/go.mod h1:3EEqHnBxQGHXRYq3HT1WyXAvT7LLY3tl70hw6tQIbjI=
|
||||
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
||||
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20191009163259-e802c2cb94ae/go.mod h1:mjwGPas4yKduTyubHvD1Atl9r1rUq8DfVy+gkVvZ+oo=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
|
||||
|
@ -70,7 +114,10 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
|
|||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
|
||||
github.com/aws/aws-sdk-go v1.19.18/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.31.13/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
|
||||
github.com/aws/aws-sdk-go v1.35.30 h1:ZT+70Tw1ar5U2bL81ZyIvcLorxlD1UoxoIgjsEkismY=
|
||||
github.com/aws/aws-sdk-go v1.35.30/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
|
@ -83,6 +130,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
|||
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
|
@ -113,8 +161,11 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/devigned/tab v0.1.1/go.mod h1:XG9mPq0dFghrYvoBF3xdRrJzSTX1b7IQrvaL9mzjeJY=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
|
||||
github.com/drakkan/crypto v0.0.0-20201118124913-1ba5185435c1 h1:a+m/8QzsuNhWTVBprULP341v5EZZeKLleVSDcOiAS0c=
|
||||
github.com/drakkan/crypto v0.0.0-20201118124913-1ba5185435c1/go.mod h1:HCh3rfXxsHzqOEbzc/nqz6WnUhb7Nv19n/o64V0Zmbg=
|
||||
github.com/drakkan/ftp v0.0.0-20201114075148-9b9adce499a9 h1:LPH1dEblAOO/LoG7yHPMtBLXhQmjaga91/DDjWk9jWA=
|
||||
|
@ -134,8 +185,10 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m
|
|||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fclairamb/ftpserverlib v0.9.1-0.20201105003045-1edd6bf7ae53 h1:veX6+jZG1119HXbWAbU2tQ99Zz5BSaFf7tfLgjLjZGI=
|
||||
github.com/fclairamb/ftpserverlib v0.9.1-0.20201105003045-1edd6bf7ae53/go.mod h1:sMPjxPuoVwwoV87gdPkyTb0dVofmCKpVZCQ3rMVokjc=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4=
|
||||
github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
|
@ -151,18 +204,24 @@ github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1
|
|||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo=
|
||||
github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
|
@ -197,6 +256,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
|
|||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
|
@ -206,10 +267,18 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-replayers/grpcreplay v0.1.0 h1:eNb1y9rZFmY4ax45uEEECSa8fsxGRU+8Bil52ASAwic=
|
||||
github.com/google/go-replayers/grpcreplay v0.1.0/go.mod h1:8Ig2Idjpr6gifRd6pNVggX6TC1Zw6Jx74AKp7QNH2QE=
|
||||
github.com/google/go-replayers/httpreplay v0.1.0 h1:AX7FUb4BjrrzNvblr/OlgwrmFiep6soj5K2QSDW7BGk=
|
||||
github.com/google/go-replayers/httpreplay v0.1.0/go.mod h1:YKZViNhiGgqdBlUbI2MwGpq4pXxNmhJLPHQ7cv2b5no=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible h1:xmapqc1AyLoB+ddYT6r04bD9lIjlOqGaREovi0SzFaE=
|
||||
github.com/google/martian v2.1.1-0.20190517191504-25dcb96d9e51+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60=
|
||||
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
|
@ -217,19 +286,26 @@ github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hf
|
|||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200507031123-427632fa3b1c/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/wire v0.4.0 h1:kXcsA/rIGzJImVqPdhfnr6q0xsS9gU0515q1EPpJ9fE=
|
||||
github.com/google/wire v0.4.0/go.mod h1:ngWDr9Qvq3yZA10YrxfyGELY/AFWGVpy9c1LTRi1EoU=
|
||||
github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww=
|
||||
github.com/googleapis/gax-go v2.0.2+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||
|
@ -247,16 +323,29 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt
|
|||
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
|
||||
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
|
||||
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-plugin v1.0.0/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
|
||||
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
|
@ -267,16 +356,27 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO
|
|||
github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
|
||||
github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
|
||||
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
|
||||
github.com/hashicorp/vault/api v1.0.2 h1:/V9fULvLwt58vme/6Rkt/p/GtlresQv+Z9E6dgdANhs=
|
||||
github.com/hashicorp/vault/api v1.0.2/go.mod h1:AV/+M5VPDpB90arloVX0rVDUIHkONiwz5Uza9HRtpUE=
|
||||
github.com/hashicorp/vault/sdk v0.1.8 h1:pfF3KwA1yPlfpmcumNsFM4uo91WMasX5gTuIkItu9r0=
|
||||
github.com/hashicorp/vault/sdk v0.1.8/go.mod h1:tHZfc6St71twLizWNHvnnbiGFo1aq0eD2jGPLtP8kAU=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
|
@ -284,7 +384,9 @@ github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
|
|||
github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
|
@ -297,8 +399,10 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
|||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
|
||||
|
@ -308,6 +412,8 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
|
|||
github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY=
|
||||
github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||
github.com/mattn/go-ieproxy v0.0.0-20190702010315-6dee0af9227d/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||
github.com/mattn/go-ieproxy v0.0.1 h1:qiyop7gCflfhwCzGyeT0gro3sF9AIg9HU98JORTkqfI=
|
||||
github.com/mattn/go-ieproxy v0.0.1/go.mod h1:pYabZ6IHcRpFh7vIaLfK7rdcWgFEb3SFJ6/gNWuh88E=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
|
@ -325,15 +431,20 @@ github.com/miekg/dns v1.1.35/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7
|
|||
github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU=
|
||||
github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8=
|
||||
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
|
@ -347,6 +458,7 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
|
|||
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
|
||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||
|
@ -367,17 +479,21 @@ github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnh
|
|||
github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k=
|
||||
github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw=
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||
github.com/otiai10/curr v1.0.0 h1:TJIWdbX0B+kpNagQrjgq8bCMrbhiuX73M2XwgtDMoOI=
|
||||
github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs=
|
||||
github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo=
|
||||
github.com/otiai10/mint v1.3.1 h1:BCmzIS3n71sGfHB5NMNDB3lHYPz8fWSkCAErHed//qc=
|
||||
github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac=
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
|
||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pires/go-proxyproto v0.3.2 h1:E5ig1h9SFGne7IWVY6yRu3UCzyAFkQIukXHMkdFUOCA=
|
||||
github.com/pires/go-proxyproto v0.3.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
|
||||
|
@ -438,14 +554,20 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4/go.mod h1:MnkX001NG75g3p8bhFycnyIjeQoOjGL6CEIsdE/nKSY=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
|
@ -474,6 +596,7 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3
|
|||
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
|
@ -500,6 +623,7 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
|||
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
|
||||
go.opencensus.io v0.15.0/go.mod h1:UffZAU+4sDEINUGP/B7UfBBkq4fqLu9zXAX7ke6CHW0=
|
||||
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
|
@ -519,6 +643,10 @@ go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+
|
|||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
gocloud.dev v0.20.0 h1:mbEKMfnyPV7W1Rj35R1xXfjszs9dXkwSOq2KoFr25g8=
|
||||
gocloud.dev v0.20.0/go.mod h1:+Y/RpSXrJthIOM8uFNzWp6MRu9pFPNFEEZrQMxpkfIc=
|
||||
gocloud.dev/secrets/hashivault v0.20.0 h1:919urzRWksXrZNNqqlHvek9IE6lWvYM4m8dvFqXq4HU=
|
||||
gocloud.dev/secrets/hashivault v0.20.0/go.mod h1:2nNlZ76i4JlT9qrPVKbINB01L8BGgv4wmq2Cqyz22dA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -540,6 +668,7 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl
|
|||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
|
@ -548,9 +677,11 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
|||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -565,6 +696,7 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -574,8 +706,10 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -600,11 +734,13 @@ golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200317113312-5766fd39f98d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -614,10 +750,12 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201117222635-ba5294a509c7 h1:s330+6z/Ko3J0o6rvOcwXe5nzs7UT9tLKHoOXYn6uE0=
|
||||
golang.org/x/sys v0.0.0-20201117222635-ba5294a509c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
|
@ -625,6 +763,7 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
@ -635,6 +774,7 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
|||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
|
@ -667,10 +807,15 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK
|
|||
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
|
||||
golang.org/x/tools v0.0.0-20200317043434-63da46f3035e/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200601175630-2caf76543d99/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200606014950-c42cb6316fb6/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200608174601-1b747fd94509/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
|
@ -680,13 +825,16 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u
|
|||
golang.org/x/tools v0.0.0-20200915173823-2db8f0ff891c/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20201118030313-598b068a9102 h1:kr6Ik/EJgxdTSLX+rSiDounHdHWMBu9Ks/ghr2hWNpo=
|
||||
golang.org/x/tools v0.0.0-20201118030313-598b068a9102/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
|
@ -699,6 +847,7 @@ google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/
|
|||
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
|
||||
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.26.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
|
||||
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
|
||||
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
|
||||
|
@ -713,12 +862,15 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
|
|||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
|
@ -735,11 +887,15 @@ google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfG
|
|||
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200317114155-1f3552e48f24/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200325114520-5b2d0af7952b/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20200603110839-e855014d5736/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200608115520-7c474a2e3482/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
|
@ -751,8 +907,10 @@ google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6D
|
|||
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20201117123952-62d171c70ae1 h1:EVow1AaDgdoMjdO64/fntn4+RGTVor8YE/mkmIYsqFM=
|
||||
google.golang.org/genproto v0.0.0-20201117123952-62d171c70ae1/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
|
@ -784,11 +942,14 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
|
|||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
|
||||
gopkg.in/dutchcoders/goftp.v1 v1.0.0-20170301105846-ed59a591ce14 h1:tHqNpm9sPaE6BSuMLXBzgTwukQLdBEt4OYU2coQjEQQ=
|
||||
gopkg.in/dutchcoders/goftp.v1 v1.0.0-20170301105846-ed59a591ce14/go.mod h1:nzmlZQ+UqB5+55CRTV/dOaiK8OrPl6Co96Ob8lH4Wxw=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
|
@ -801,6 +962,8 @@ gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
|||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
|
||||
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
)
|
||||
|
||||
|
@ -82,6 +83,7 @@ func addUser(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user.SetEmptySecretsIfNil()
|
||||
switch user.FsConfig.Provider {
|
||||
case dataprovider.S3FilesystemProvider:
|
||||
if user.FsConfig.S3Config.AccessSecret.IsRedacted() {
|
||||
|
@ -136,9 +138,9 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
currentPermissions := user.Permissions
|
||||
var currentS3AccessSecret vfs.Secret
|
||||
var currentAzAccountKey vfs.Secret
|
||||
var currentGCSCredentials vfs.Secret
|
||||
var currentS3AccessSecret *kms.Secret
|
||||
var currentAzAccountKey *kms.Secret
|
||||
var currentGCSCredentials *kms.Secret
|
||||
if user.FsConfig.Provider == dataprovider.S3FilesystemProvider {
|
||||
currentS3AccessSecret = user.FsConfig.S3Config.AccessSecret
|
||||
}
|
||||
|
@ -157,6 +159,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
user.SetEmptySecretsIfNil()
|
||||
// we use new Permissions if passed otherwise the old ones
|
||||
if len(user.Permissions) == 0 {
|
||||
user.Permissions = currentPermissions
|
||||
|
@ -207,7 +210,7 @@ func disconnectUser(username string) {
|
|||
}
|
||||
}
|
||||
|
||||
func updateEncryptedSecrets(user *dataprovider.User, currentS3AccessSecret, currentAzAccountKey, currentGCSCredentials vfs.Secret) {
|
||||
func updateEncryptedSecrets(user *dataprovider.User, currentS3AccessSecret, currentAzAccountKey, currentGCSCredentials *kms.Secret) {
|
||||
// we use the new access secret if plain or empty, otherwise the old value
|
||||
if user.FsConfig.Provider == dataprovider.S3FilesystemProvider {
|
||||
if !user.FsConfig.S3Config.AccessSecret.IsPlain() && !user.FsConfig.S3Config.AccessSecret.IsEmpty() {
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/httpclient"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/drakkan/sftpgo/version"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
|
@ -710,19 +711,41 @@ func compareAzBlobConfig(expected *dataprovider.User, actual *dataprovider.User)
|
|||
return nil
|
||||
}
|
||||
|
||||
func checkEncryptedSecret(expected, actual vfs.Secret) error {
|
||||
func areSecretEquals(expected, actual *kms.Secret) bool {
|
||||
if expected == nil && actual == nil {
|
||||
return true
|
||||
}
|
||||
if expected != nil && expected.IsEmpty() && actual == nil {
|
||||
return true
|
||||
}
|
||||
if actual != nil && actual.IsEmpty() && expected == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkEncryptedSecret(expected, actual *kms.Secret) error {
|
||||
if areSecretEquals(expected, actual) {
|
||||
return nil
|
||||
}
|
||||
if expected == nil && actual != nil && !actual.IsEmpty() {
|
||||
return errors.New("secret mismatch")
|
||||
}
|
||||
if actual == nil && expected != nil && !expected.IsEmpty() {
|
||||
return errors.New("secret mismatch")
|
||||
}
|
||||
if expected.IsPlain() && actual.IsEncrypted() {
|
||||
if actual.Payload == "" {
|
||||
if actual.GetPayload() == "" {
|
||||
return errors.New("invalid secret payload")
|
||||
}
|
||||
if actual.AdditionalData != "" {
|
||||
if actual.GetAdditionalData() != "" {
|
||||
return errors.New("invalid secret additional data")
|
||||
}
|
||||
if actual.Key != "" {
|
||||
if actual.GetKey() != "" {
|
||||
return errors.New("invalid secret key")
|
||||
}
|
||||
} else {
|
||||
if expected.Status != actual.Status || expected.Payload != actual.Payload {
|
||||
if expected.GetStatus() != actual.GetStatus() || expected.GetPayload() != actual.GetPayload() {
|
||||
return errors.New("secret mismatch")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/config"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
|
@ -140,6 +141,12 @@ func TestMain(m *testing.M) {
|
|||
|
||||
httpConfig := config.GetHTTPConfig()
|
||||
httpConfig.Initialize(configDir)
|
||||
kmsConfig := config.GetKMSConfig()
|
||||
err = kmsConfig.Initialize()
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("error initializing kms: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
httpdConf := config.GetHTTPDConfig()
|
||||
|
||||
|
@ -191,7 +198,7 @@ func TestMain(m *testing.M) {
|
|||
defer testServer.Close()
|
||||
|
||||
exitCode := m.Run()
|
||||
//os.Remove(logfilePath) //nolint:errcheck
|
||||
os.Remove(logfilePath) //nolint:errcheck
|
||||
os.RemoveAll(backupsPath) //nolint:errcheck
|
||||
os.RemoveAll(credentialsPath) //nolint:errcheck
|
||||
os.Remove(certPath) //nolint:errcheck
|
||||
|
@ -439,14 +446,13 @@ func TestAddUserInvalidFsConfig(t *testing.T) {
|
|||
u.FsConfig.S3Config.Bucket = "testbucket"
|
||||
u.FsConfig.S3Config.Region = "eu-west-1"
|
||||
u.FsConfig.S3Config.AccessKey = "access-key"
|
||||
u.FsConfig.S3Config.AccessSecret.Payload = "access-secret"
|
||||
u.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusRedacted
|
||||
u.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusRedacted, "access-secret", "", "")
|
||||
u.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?a=b"
|
||||
u.FsConfig.S3Config.StorageClass = "Standard" //nolint:goconst
|
||||
u.FsConfig.S3Config.KeyPrefix = "/adir/subdir/"
|
||||
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
u.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain
|
||||
u.FsConfig.S3Config.AccessSecret.SetStatus(kms.SecretStatusPlain)
|
||||
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
u.FsConfig.S3Config.KeyPrefix = ""
|
||||
|
@ -468,20 +474,18 @@ func TestAddUserInvalidFsConfig(t *testing.T) {
|
|||
u.FsConfig.GCSConfig.Bucket = "abucket"
|
||||
u.FsConfig.GCSConfig.StorageClass = "Standard"
|
||||
u.FsConfig.GCSConfig.KeyPrefix = "/somedir/subdir/"
|
||||
u.FsConfig.GCSConfig.Credentials.Payload = "test" //nolint:goconst
|
||||
u.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusRedacted
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusRedacted, "test", "", "") //nolint:goconst
|
||||
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
u.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusPlain
|
||||
u.FsConfig.GCSConfig.Credentials.SetStatus(kms.SecretStatusPlain)
|
||||
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
u.FsConfig.GCSConfig.KeyPrefix = "somedir/subdir/" //nolint:goconst
|
||||
u.FsConfig.GCSConfig.Credentials = vfs.Secret{}
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
|
||||
u.FsConfig.GCSConfig.AutomaticCredentials = 0
|
||||
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
u.FsConfig.GCSConfig.Credentials.Payload = "invalid"
|
||||
u.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusAES256GCM
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusSecretBox, "invalid", "", "")
|
||||
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -497,12 +501,11 @@ func TestAddUserInvalidFsConfig(t *testing.T) {
|
|||
u.FsConfig.AzBlobConfig.Container = "container"
|
||||
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
u.FsConfig.AzBlobConfig.AccountKey.Payload = "key"
|
||||
u.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusRedacted
|
||||
u.FsConfig.AzBlobConfig.AccountKey = kms.NewSecret(kms.SecretStatusRedacted, "key", "", "")
|
||||
u.FsConfig.AzBlobConfig.KeyPrefix = "/amedir/subdir/"
|
||||
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
u.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusPlain
|
||||
u.FsConfig.AzBlobConfig.AccountKey.SetStatus(kms.SecretStatusPlain)
|
||||
_, _, err = httpd.AddUser(u, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
u.FsConfig.AzBlobConfig.KeyPrefix = "amedir/subdir/"
|
||||
|
@ -1013,35 +1016,31 @@ func TestUserS3Config(t *testing.T) {
|
|||
user.FsConfig.S3Config.Bucket = "test" //nolint:goconst
|
||||
user.FsConfig.S3Config.Region = "us-east-1" //nolint:goconst
|
||||
user.FsConfig.S3Config.AccessKey = "Server-Access-Key"
|
||||
user.FsConfig.S3Config.AccessSecret.Payload = "Server-Access-Secret"
|
||||
user.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain
|
||||
user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("Server-Access-Secret")
|
||||
user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000"
|
||||
user.FsConfig.S3Config.UploadPartSize = 8
|
||||
user, body, err := httpd.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err, string(body))
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.S3Config.AccessSecret.Status)
|
||||
assert.NotEmpty(t, user.FsConfig.S3Config.AccessSecret.Payload)
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.AdditionalData)
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.Key)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus())
|
||||
assert.NotEmpty(t, user.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey())
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
user.Password = defaultPassword
|
||||
user.ID = 0
|
||||
secret := vfs.Secret{
|
||||
Payload: "Server-Access-Secret",
|
||||
Status: vfs.SecretStatusAES256GCM,
|
||||
}
|
||||
secret := kms.NewSecret(kms.SecretStatusSecretBox, "Server-Access-Secret", "", "")
|
||||
user.FsConfig.S3Config.AccessSecret = secret
|
||||
_, _, err = httpd.AddUser(user, http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
user.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain
|
||||
user.FsConfig.S3Config.AccessSecret.SetStatus(kms.SecretStatusPlain)
|
||||
user, _, err = httpd.AddUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
initialSecretPayload := user.FsConfig.S3Config.AccessSecret.Payload
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.S3Config.AccessSecret.Status)
|
||||
initialSecretPayload := user.FsConfig.S3Config.AccessSecret.GetPayload()
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus())
|
||||
assert.NotEmpty(t, initialSecretPayload)
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.AdditionalData)
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.Key)
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey())
|
||||
user.FsConfig.Provider = dataprovider.S3FilesystemProvider
|
||||
user.FsConfig.S3Config.Bucket = "test-bucket"
|
||||
user.FsConfig.S3Config.Region = "us-east-1" //nolint:goconst
|
||||
|
@ -1051,16 +1050,16 @@ func TestUserS3Config(t *testing.T) {
|
|||
user.FsConfig.S3Config.UploadConcurrency = 5
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.S3Config.AccessSecret.Status)
|
||||
assert.Equal(t, initialSecretPayload, user.FsConfig.S3Config.AccessSecret.Payload)
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.AdditionalData)
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.Key)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.S3Config.AccessSecret.GetStatus())
|
||||
assert.Equal(t, initialSecretPayload, user.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.S3Config.AccessSecret.GetKey())
|
||||
// test user without access key and access secret (shared config state)
|
||||
user.FsConfig.Provider = dataprovider.S3FilesystemProvider
|
||||
user.FsConfig.S3Config.Bucket = "testbucket"
|
||||
user.FsConfig.S3Config.Region = "us-east-1"
|
||||
user.FsConfig.S3Config.AccessKey = ""
|
||||
user.FsConfig.S3Config.AccessSecret = vfs.Secret{}
|
||||
user.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
user.FsConfig.S3Config.Endpoint = ""
|
||||
user.FsConfig.S3Config.KeyPrefix = "somedir/subdir"
|
||||
user.FsConfig.S3Config.UploadPartSize = 6
|
||||
|
@ -1089,49 +1088,46 @@ func TestUserGCSConfig(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
user.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
user.FsConfig.GCSConfig.Bucket = "test"
|
||||
user.FsConfig.GCSConfig.Credentials.Payload = "fake credentials" //nolint:goconst
|
||||
user.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusPlain
|
||||
user.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials") //nolint:goconst
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
credentialFile := filepath.Join(credentialsPath, fmt.Sprintf("%v_gcs_credentials.json", user.Username))
|
||||
assert.FileExists(t, credentialFile)
|
||||
creds, err := ioutil.ReadFile(credentialFile)
|
||||
assert.NoError(t, err)
|
||||
secret := &vfs.Secret{}
|
||||
secret := kms.NewEmptySecret()
|
||||
err = json.Unmarshal(creds, secret)
|
||||
assert.NoError(t, err)
|
||||
err = secret.Decrypt()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "fake credentials", secret.Payload)
|
||||
user.FsConfig.GCSConfig.Credentials.Payload = "fake encrypted credentials"
|
||||
user.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusAES256GCM
|
||||
assert.Equal(t, "fake credentials", secret.GetPayload())
|
||||
user.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusSecretBox, "fake encrypted credentials", "", "")
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
assert.FileExists(t, credentialFile)
|
||||
creds, err = ioutil.ReadFile(credentialFile)
|
||||
assert.NoError(t, err)
|
||||
secret = &vfs.Secret{}
|
||||
secret = kms.NewEmptySecret()
|
||||
err = json.Unmarshal(creds, secret)
|
||||
assert.NoError(t, err)
|
||||
err = secret.Decrypt()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "fake credentials", secret.Payload)
|
||||
assert.Equal(t, "fake credentials", secret.GetPayload())
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
user.Password = defaultPassword
|
||||
user.ID = 0
|
||||
user.FsConfig.GCSConfig.Credentials.Payload = "fake credentials"
|
||||
user.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusAES256GCM
|
||||
user.FsConfig.GCSConfig.Credentials = kms.NewSecret(kms.SecretStatusSecretBox, "fake credentials", "", "")
|
||||
_, _, err = httpd.AddUser(user, http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
user.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusPlain
|
||||
user.FsConfig.GCSConfig.Credentials.SetStatus(kms.SecretStatusPlain)
|
||||
user, body, err := httpd.AddUser(user, http.StatusOK)
|
||||
assert.NoError(t, err, string(body))
|
||||
err = os.RemoveAll(credentialsPath)
|
||||
assert.NoError(t, err)
|
||||
err = os.MkdirAll(credentialsPath, 0700)
|
||||
assert.NoError(t, err)
|
||||
user.FsConfig.GCSConfig.Credentials = vfs.Secret{}
|
||||
user.FsConfig.GCSConfig.Credentials = kms.NewEmptySecret()
|
||||
user.FsConfig.GCSConfig.AutomaticCredentials = 1
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
|
@ -1141,8 +1137,7 @@ func TestUserGCSConfig(t *testing.T) {
|
|||
user.FsConfig.S3Config.Bucket = "test1"
|
||||
user.FsConfig.S3Config.Region = "us-east-1"
|
||||
user.FsConfig.S3Config.AccessKey = "Server-Access-Key1"
|
||||
user.FsConfig.S3Config.AccessSecret.Payload = "secret"
|
||||
user.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain
|
||||
user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("secret")
|
||||
user.FsConfig.S3Config.Endpoint = "http://localhost:9000"
|
||||
user.FsConfig.S3Config.KeyPrefix = "somedir/subdir"
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
|
@ -1150,8 +1145,7 @@ func TestUserGCSConfig(t *testing.T) {
|
|||
user.FsConfig.S3Config = vfs.S3FsConfig{}
|
||||
user.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
user.FsConfig.GCSConfig.Bucket = "test1"
|
||||
user.FsConfig.GCSConfig.Credentials.Payload = "fake credentials"
|
||||
user.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusPlain
|
||||
user.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials")
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -1165,49 +1159,42 @@ func TestUserAzureBlobConfig(t *testing.T) {
|
|||
user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider
|
||||
user.FsConfig.AzBlobConfig.Container = "test"
|
||||
user.FsConfig.AzBlobConfig.AccountName = "Server-Account-Name"
|
||||
user.FsConfig.AzBlobConfig.AccountKey.Payload = "Server-Account-Key"
|
||||
user.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusPlain
|
||||
user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key")
|
||||
user.FsConfig.AzBlobConfig.Endpoint = "http://127.0.0.1:9000"
|
||||
user.FsConfig.AzBlobConfig.UploadPartSize = 8
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
initialPayload := user.FsConfig.AzBlobConfig.AccountKey.Payload
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.AzBlobConfig.AccountKey.Status)
|
||||
initialPayload := user.FsConfig.AzBlobConfig.AccountKey.GetPayload()
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus())
|
||||
assert.NotEmpty(t, initialPayload)
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.AdditionalData)
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.Key)
|
||||
user.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusAES256GCM
|
||||
user.FsConfig.AzBlobConfig.AccountKey.AdditionalData = "data"
|
||||
user.FsConfig.AzBlobConfig.AccountKey.Key = "fake key"
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey())
|
||||
user.FsConfig.AzBlobConfig.AccountKey.SetStatus(kms.SecretStatusSecretBox)
|
||||
user.FsConfig.AzBlobConfig.AccountKey.SetAdditionalData("data")
|
||||
user.FsConfig.AzBlobConfig.AccountKey.SetKey("fake key")
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.AzBlobConfig.AccountKey.Status)
|
||||
assert.Equal(t, initialPayload, user.FsConfig.AzBlobConfig.AccountKey.Payload)
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.AdditionalData)
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.Key)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus())
|
||||
assert.Equal(t, initialPayload, user.FsConfig.AzBlobConfig.AccountKey.GetPayload())
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey())
|
||||
|
||||
_, err = httpd.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
user.Password = defaultPassword
|
||||
user.ID = 0
|
||||
secret := vfs.Secret{
|
||||
Payload: "Server-Account-Key",
|
||||
Status: vfs.SecretStatusAES256GCM,
|
||||
}
|
||||
secret := kms.NewSecret(kms.SecretStatusSecretBox, "Server-Account-Key", "", "")
|
||||
user.FsConfig.AzBlobConfig.AccountKey = secret
|
||||
_, _, err = httpd.AddUser(user, http.StatusOK)
|
||||
assert.Error(t, err)
|
||||
user.FsConfig.AzBlobConfig.AccountKey = vfs.Secret{
|
||||
Payload: "Server-Account-Key-Test",
|
||||
Status: vfs.SecretStatusPlain,
|
||||
}
|
||||
user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key-Test")
|
||||
user, _, err = httpd.AddUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
initialPayload = user.FsConfig.AzBlobConfig.AccountKey.Payload
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.AzBlobConfig.AccountKey.Status)
|
||||
initialPayload = user.FsConfig.AzBlobConfig.AccountKey.GetPayload()
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus())
|
||||
assert.NotEmpty(t, initialPayload)
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.AdditionalData)
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.Key)
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey())
|
||||
user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider
|
||||
user.FsConfig.AzBlobConfig.Container = "test-container"
|
||||
user.FsConfig.AzBlobConfig.Endpoint = "http://localhost:9001"
|
||||
|
@ -1215,16 +1202,16 @@ func TestUserAzureBlobConfig(t *testing.T) {
|
|||
user.FsConfig.AzBlobConfig.UploadConcurrency = 5
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.AzBlobConfig.AccountKey.Status)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.AzBlobConfig.AccountKey.GetStatus())
|
||||
assert.NotEmpty(t, initialPayload)
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.AdditionalData)
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.Key)
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.AzBlobConfig.AccountKey.GetKey())
|
||||
// test user without access key and access secret (sas)
|
||||
user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider
|
||||
user.FsConfig.AzBlobConfig.SASURL = "https://myaccount.blob.core.windows.net/pictures/profile.jpg?sv=2012-02-12&st=2009-02-09&se=2009-02-10&sr=c&sp=r&si=YWJjZGVmZw%3d%3d&sig=dD80ihBh5jfNpymO5Hg1IdiJIEvHcJpCMiCMnN%2fRnbI%3d"
|
||||
user.FsConfig.AzBlobConfig.KeyPrefix = "somedir/subdir"
|
||||
user.FsConfig.AzBlobConfig.AccountName = ""
|
||||
user.FsConfig.AzBlobConfig.AccountKey = vfs.Secret{}
|
||||
user.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()
|
||||
user.FsConfig.AzBlobConfig.UploadPartSize = 6
|
||||
user.FsConfig.AzBlobConfig.UploadConcurrency = 4
|
||||
user, _, err = httpd.UpdateUser(user, http.StatusOK, "")
|
||||
|
@ -1260,8 +1247,7 @@ func TestUserHiddenFields(t *testing.T) {
|
|||
u1.FsConfig.S3Config.Bucket = "test"
|
||||
u1.FsConfig.S3Config.Region = "us-east-1"
|
||||
u1.FsConfig.S3Config.AccessKey = "S3-Access-Key"
|
||||
u1.FsConfig.S3Config.AccessSecret.Payload = "S3-Access-Secret"
|
||||
u1.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain
|
||||
u1.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("S3-Access-Secret")
|
||||
user1, _, err := httpd.AddUser(u1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -1269,8 +1255,7 @@ func TestUserHiddenFields(t *testing.T) {
|
|||
u2.Username = usernames[1]
|
||||
u2.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
u2.FsConfig.GCSConfig.Bucket = "test"
|
||||
u2.FsConfig.GCSConfig.Credentials.Payload = "fake credentials"
|
||||
u2.FsConfig.GCSConfig.Credentials.Status = vfs.SecretStatusPlain
|
||||
u2.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("fake credentials")
|
||||
user2, _, err := httpd.AddUser(u2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -1279,8 +1264,7 @@ func TestUserHiddenFields(t *testing.T) {
|
|||
u3.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider
|
||||
u3.FsConfig.AzBlobConfig.Container = "test"
|
||||
u3.FsConfig.AzBlobConfig.AccountName = "Server-Account-Name"
|
||||
u3.FsConfig.AzBlobConfig.AccountKey.Payload = "Server-Account-Key"
|
||||
u3.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusPlain
|
||||
u3.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("Server-Account-Key")
|
||||
user3, _, err := httpd.AddUser(u3, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -1298,69 +1282,69 @@ func TestUserHiddenFields(t *testing.T) {
|
|||
user1, _, err = httpd.GetUserByID(user1.ID, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, user1.Password)
|
||||
assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.Key)
|
||||
assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.AdditionalData)
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.Status)
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.Payload)
|
||||
assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetKey())
|
||||
assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetAdditionalData())
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetStatus())
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
|
||||
user2, _, err = httpd.GetUserByID(user2.ID, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, user2.Password)
|
||||
assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.Key)
|
||||
assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.AdditionalData)
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.Status)
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.Payload)
|
||||
assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetKey())
|
||||
assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData())
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetStatus())
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetPayload())
|
||||
|
||||
user3, _, err = httpd.GetUserByID(user3.ID, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, user3.Password)
|
||||
assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.Key)
|
||||
assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.AdditionalData)
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.Status)
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.Payload)
|
||||
assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey())
|
||||
assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetStatus())
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetPayload())
|
||||
|
||||
// finally check that we have all the data inside the data provider
|
||||
user1, err = dataprovider.GetUserByID(user1.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, user1.Password)
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.Key)
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.AdditionalData)
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.Status)
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.Payload)
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetKey())
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetAdditionalData())
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetStatus())
|
||||
assert.NotEmpty(t, user1.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
err = user1.FsConfig.S3Config.AccessSecret.Decrypt()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, vfs.SecretStatusPlain, user1.FsConfig.S3Config.AccessSecret.Status)
|
||||
assert.Equal(t, u1.FsConfig.S3Config.AccessSecret.Payload, user1.FsConfig.S3Config.AccessSecret.Payload)
|
||||
assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.Key)
|
||||
assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.AdditionalData)
|
||||
assert.Equal(t, kms.SecretStatusPlain, user1.FsConfig.S3Config.AccessSecret.GetStatus())
|
||||
assert.Equal(t, u1.FsConfig.S3Config.AccessSecret.GetPayload(), user1.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetKey())
|
||||
assert.Empty(t, user1.FsConfig.S3Config.AccessSecret.GetAdditionalData())
|
||||
|
||||
user2, err = dataprovider.GetUserByID(user2.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, user2.Password)
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.Key)
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.AdditionalData)
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.Status)
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.Payload)
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetKey())
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData())
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetStatus())
|
||||
assert.NotEmpty(t, user2.FsConfig.GCSConfig.Credentials.GetPayload())
|
||||
err = user2.FsConfig.GCSConfig.Credentials.Decrypt()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, vfs.SecretStatusPlain, user2.FsConfig.GCSConfig.Credentials.Status)
|
||||
assert.Equal(t, u2.FsConfig.GCSConfig.Credentials.Payload, user2.FsConfig.GCSConfig.Credentials.Payload)
|
||||
assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.Key)
|
||||
assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.AdditionalData)
|
||||
assert.Equal(t, kms.SecretStatusPlain, user2.FsConfig.GCSConfig.Credentials.GetStatus())
|
||||
assert.Equal(t, u2.FsConfig.GCSConfig.Credentials.GetPayload(), user2.FsConfig.GCSConfig.Credentials.GetPayload())
|
||||
assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetKey())
|
||||
assert.Empty(t, user2.FsConfig.GCSConfig.Credentials.GetAdditionalData())
|
||||
|
||||
user3, err = dataprovider.GetUserByID(user3.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, user3.Password)
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.Key)
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.AdditionalData)
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.Status)
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.Payload)
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey())
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetStatus())
|
||||
assert.NotEmpty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetPayload())
|
||||
err = user3.FsConfig.AzBlobConfig.AccountKey.Decrypt()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, vfs.SecretStatusPlain, user3.FsConfig.AzBlobConfig.AccountKey.Status)
|
||||
assert.Equal(t, u3.FsConfig.AzBlobConfig.AccountKey.Payload, user3.FsConfig.AzBlobConfig.AccountKey.Payload)
|
||||
assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.Key)
|
||||
assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.AdditionalData)
|
||||
assert.Equal(t, kms.SecretStatusPlain, user3.FsConfig.AzBlobConfig.AccountKey.GetStatus())
|
||||
assert.Equal(t, u3.FsConfig.AzBlobConfig.AccountKey.GetPayload(), user3.FsConfig.AzBlobConfig.AccountKey.GetPayload())
|
||||
assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetKey())
|
||||
assert.Empty(t, user3.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
|
||||
|
||||
_, err = httpd.RemoveUser(user1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -1382,31 +1366,28 @@ func TestUserHiddenFields(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSecretObject(t *testing.T) {
|
||||
s := vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: "test data",
|
||||
AdditionalData: "username",
|
||||
}
|
||||
s := kms.NewPlainSecret("test data")
|
||||
s.SetAdditionalData("username")
|
||||
require.True(t, s.IsValid())
|
||||
err := s.Encrypt()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, vfs.SecretStatusAES256GCM, s.Status)
|
||||
require.NotEmpty(t, s.Payload)
|
||||
require.NotEmpty(t, s.Key)
|
||||
require.Equal(t, kms.SecretStatusSecretBox, s.GetStatus())
|
||||
require.NotEmpty(t, s.GetPayload())
|
||||
require.NotEmpty(t, s.GetKey())
|
||||
require.True(t, s.IsValid())
|
||||
err = s.Decrypt()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, vfs.SecretStatusPlain, s.Status)
|
||||
require.Equal(t, "test data", s.Payload)
|
||||
require.Empty(t, s.Key)
|
||||
require.Equal(t, kms.SecretStatusPlain, s.GetStatus())
|
||||
require.Equal(t, "test data", s.GetPayload())
|
||||
require.Empty(t, s.GetKey())
|
||||
|
||||
oldFormat := "$aes$5b97e3a3324a2f53e2357483383367c0$0ed3132b584742ab217866219da633266782b69b13e50ebc6ddfb7c4fbf2f2a414c6d5f813"
|
||||
s, err = vfs.GetSecretFromCompatString(oldFormat)
|
||||
s, err = kms.GetSecretFromCompatString(oldFormat)
|
||||
require.NoError(t, err)
|
||||
require.True(t, s.IsValid())
|
||||
require.Equal(t, vfs.SecretStatusPlain, s.Status)
|
||||
require.Equal(t, "test data", s.Payload)
|
||||
require.Empty(t, s.Key)
|
||||
require.Equal(t, kms.SecretStatusPlain, s.GetStatus())
|
||||
require.Equal(t, "test data", s.GetPayload())
|
||||
require.Empty(t, s.GetKey())
|
||||
}
|
||||
|
||||
func TestUpdateUserNoCredentials(t *testing.T) {
|
||||
|
@ -3003,8 +2984,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
user.FsConfig.S3Config.Bucket = "test"
|
||||
user.FsConfig.S3Config.Region = "eu-west-1"
|
||||
user.FsConfig.S3Config.AccessKey = "access-key"
|
||||
user.FsConfig.S3Config.AccessSecret.Payload = "access-secret"
|
||||
user.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain
|
||||
user.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret("access-secret")
|
||||
user.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/path?a=b"
|
||||
user.FsConfig.S3Config.StorageClass = "Standard"
|
||||
user.FsConfig.S3Config.KeyPrefix = "somedir/subdir/"
|
||||
|
@ -3030,7 +3010,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
form.Set("s3_bucket", user.FsConfig.S3Config.Bucket)
|
||||
form.Set("s3_region", user.FsConfig.S3Config.Region)
|
||||
form.Set("s3_access_key", user.FsConfig.S3Config.AccessKey)
|
||||
form.Set("s3_access_secret", user.FsConfig.S3Config.AccessSecret.Payload)
|
||||
form.Set("s3_access_secret", user.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
form.Set("s3_storage_class", user.FsConfig.S3Config.StorageClass)
|
||||
form.Set("s3_endpoint", user.FsConfig.S3Config.Endpoint)
|
||||
form.Set("s3_key_prefix", user.FsConfig.S3Config.KeyPrefix)
|
||||
|
@ -3077,10 +3057,10 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
assert.Equal(t, updateUser.FsConfig.S3Config.UploadPartSize, user.FsConfig.S3Config.UploadPartSize)
|
||||
assert.Equal(t, updateUser.FsConfig.S3Config.UploadConcurrency, user.FsConfig.S3Config.UploadConcurrency)
|
||||
assert.Equal(t, 2, len(updateUser.Filters.FileExtensions))
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, updateUser.FsConfig.S3Config.AccessSecret.Status)
|
||||
assert.NotEmpty(t, updateUser.FsConfig.S3Config.AccessSecret.Payload)
|
||||
assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.Key)
|
||||
assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.AdditionalData)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, updateUser.FsConfig.S3Config.AccessSecret.GetStatus())
|
||||
assert.NotEmpty(t, updateUser.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetKey())
|
||||
assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetAdditionalData())
|
||||
// now check that a redacted password is not saved
|
||||
form.Set("s3_access_secret", "[**redacted**] ")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
|
@ -3096,10 +3076,10 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users))
|
||||
lastUpdatedUser := users[0]
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, lastUpdatedUser.FsConfig.S3Config.AccessSecret.Status)
|
||||
assert.Equal(t, updateUser.FsConfig.S3Config.AccessSecret.Payload, lastUpdatedUser.FsConfig.S3Config.AccessSecret.Payload)
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.Key)
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.AdditionalData)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetStatus())
|
||||
assert.Equal(t, updateUser.FsConfig.S3Config.AccessSecret.GetPayload(), lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetKey())
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.S3Config.AccessSecret.GetAdditionalData())
|
||||
// now clear credentials
|
||||
form.Set("s3_access_key", "")
|
||||
form.Set("s3_access_secret", "")
|
||||
|
@ -3222,8 +3202,7 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
|||
user.FsConfig.Provider = dataprovider.AzureBlobFilesystemProvider
|
||||
user.FsConfig.AzBlobConfig.Container = "container"
|
||||
user.FsConfig.AzBlobConfig.AccountName = "aname"
|
||||
user.FsConfig.AzBlobConfig.AccountKey.Payload = "access-skey"
|
||||
user.FsConfig.AzBlobConfig.AccountKey.Status = vfs.SecretStatusPlain
|
||||
user.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret("access-skey")
|
||||
user.FsConfig.AzBlobConfig.Endpoint = "http://127.0.0.1:9000/path?b=c"
|
||||
user.FsConfig.AzBlobConfig.KeyPrefix = "somedir/subdir/"
|
||||
user.FsConfig.AzBlobConfig.UploadPartSize = 5
|
||||
|
@ -3248,7 +3227,7 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
|||
form.Set("fs_provider", "3")
|
||||
form.Set("az_container", user.FsConfig.AzBlobConfig.Container)
|
||||
form.Set("az_account_name", user.FsConfig.AzBlobConfig.AccountName)
|
||||
form.Set("az_account_key", user.FsConfig.AzBlobConfig.AccountKey.Payload)
|
||||
form.Set("az_account_key", user.FsConfig.AzBlobConfig.AccountKey.GetPayload())
|
||||
form.Set("az_sas_url", user.FsConfig.AzBlobConfig.SASURL)
|
||||
form.Set("az_endpoint", user.FsConfig.AzBlobConfig.Endpoint)
|
||||
form.Set("az_key_prefix", user.FsConfig.AzBlobConfig.KeyPrefix)
|
||||
|
@ -3295,10 +3274,10 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
|||
assert.Equal(t, updateUser.FsConfig.AzBlobConfig.UploadPartSize, user.FsConfig.AzBlobConfig.UploadPartSize)
|
||||
assert.Equal(t, updateUser.FsConfig.AzBlobConfig.UploadConcurrency, user.FsConfig.AzBlobConfig.UploadConcurrency)
|
||||
assert.Equal(t, 2, len(updateUser.Filters.FileExtensions))
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, updateUser.FsConfig.AzBlobConfig.AccountKey.Status)
|
||||
assert.NotEmpty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.Payload)
|
||||
assert.Empty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.Key)
|
||||
assert.Empty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.AdditionalData)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, updateUser.FsConfig.AzBlobConfig.AccountKey.GetStatus())
|
||||
assert.NotEmpty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetPayload())
|
||||
assert.Empty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetKey())
|
||||
assert.Empty(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
|
||||
// now check that a redacted password is not saved
|
||||
form.Set("az_account_key", "[**redacted**] ")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
|
@ -3314,10 +3293,10 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(users))
|
||||
lastUpdatedUser := users[0]
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.Status)
|
||||
assert.Equal(t, updateUser.FsConfig.AzBlobConfig.AccountKey.Payload, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.Payload)
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.Key)
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.AdditionalData)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetStatus())
|
||||
assert.Equal(t, updateUser.FsConfig.AzBlobConfig.AccountKey.GetPayload(), lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetPayload())
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetKey())
|
||||
assert.Empty(t, lastUpdatedUser.FsConfig.AzBlobConfig.AccountKey.GetAdditionalData())
|
||||
req, _ = http.NewRequest(http.MethodDelete, userPath+"/"+strconv.FormatInt(user.ID, 10), nil)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr.Code)
|
||||
|
@ -3546,3 +3525,14 @@ func getMultipartFormData(values url.Values, fileFieldName, filePath string) (by
|
|||
err := w.Close()
|
||||
return b, w.FormDataContentType(), err
|
||||
}
|
||||
|
||||
func BenchmarkSecretDecryption(b *testing.B) {
|
||||
s := kms.NewPlainSecret("test data")
|
||||
s.SetAdditionalData("username")
|
||||
err := s.Encrypt()
|
||||
require.NoError(b, err)
|
||||
for i := 0; i < b.N; i++ {
|
||||
err = s.Clone().Decrypt()
|
||||
require.NoError(b, err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
)
|
||||
|
@ -318,8 +319,11 @@ func TestCompareUserFields(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestCompareUserFsConfig(t *testing.T) {
|
||||
secretString := "access secret"
|
||||
expected := &dataprovider.User{}
|
||||
actual := &dataprovider.User{}
|
||||
expected.SetEmptySecretsIfNil()
|
||||
actual.SetEmptySecretsIfNil()
|
||||
expected.FsConfig.Provider = dataprovider.S3FilesystemProvider
|
||||
err := compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
|
@ -336,36 +340,36 @@ func TestCompareUserFsConfig(t *testing.T) {
|
|||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.AccessKey = ""
|
||||
actual.FsConfig.S3Config.AccessSecret.Payload = "access secret"
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString)
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
secret, _ := utils.EncryptData("access secret")
|
||||
actual.FsConfig.S3Config.AccessSecret.Payload = ""
|
||||
expected.FsConfig.S3Config.AccessSecret.Payload = secret
|
||||
secret, err := utils.EncryptData(secretString)
|
||||
assert.NoError(t, err)
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
kmsSecret, err := kms.GetSecretFromCompatString(secret)
|
||||
assert.NoError(t, err)
|
||||
expected.FsConfig.S3Config.AccessSecret = kmsSecret
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.AccessSecret.Payload = "test"
|
||||
actual.FsConfig.S3Config.AccessSecret.Payload = ""
|
||||
expected.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString)
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusPlain
|
||||
actual.FsConfig.S3Config.AccessSecret.Status = vfs.SecretStatusAES256GCM
|
||||
expected.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(secretString)
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, "", "", "")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.FsConfig.S3Config.AccessSecret.Payload = "payload"
|
||||
actual.FsConfig.S3Config.AccessSecret.AdditionalData = "data"
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, secretString, "", "data")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
actual.FsConfig.S3Config.AccessSecret.AdditionalData = ""
|
||||
actual.FsConfig.S3Config.AccessSecret.Key = "key"
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewSecret(kms.SecretStatusSecretBox, secretString, "key", "")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.AccessSecret.Status = ""
|
||||
expected.FsConfig.S3Config.AccessSecret.Payload = ""
|
||||
actual.FsConfig.S3Config.AccessSecret.Status = ""
|
||||
actual.FsConfig.S3Config.AccessSecret.Payload = ""
|
||||
actual.FsConfig.S3Config.AccessSecret.AdditionalData = ""
|
||||
actual.FsConfig.S3Config.AccessSecret.Key = ""
|
||||
expected.FsConfig.S3Config.AccessSecret = nil
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
actual.FsConfig.S3Config.AccessSecret = kms.NewEmptySecret()
|
||||
expected.FsConfig.S3Config.Endpoint = "http://127.0.0.1:9000/"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
|
@ -419,10 +423,10 @@ func TestCompareUserAzureConfig(t *testing.T) {
|
|||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.AccountName = ""
|
||||
expected.FsConfig.AzBlobConfig.AccountKey.Payload = "akey"
|
||||
expected.FsConfig.AzBlobConfig.AccountKey = kms.NewSecret(kms.SecretStatusAWS, "payload", "", "")
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
expected.FsConfig.AzBlobConfig.AccountKey.Payload = ""
|
||||
expected.FsConfig.AzBlobConfig.AccountKey = kms.NewEmptySecret()
|
||||
expected.FsConfig.AzBlobConfig.Endpoint = "endpt"
|
||||
err = compareUserFsConfig(expected, actual)
|
||||
assert.Error(t, err)
|
||||
|
|
|
@ -2,7 +2,7 @@ openapi: 3.0.3
|
|||
info:
|
||||
title: SFTPGo
|
||||
description: 'SFTPGo REST API'
|
||||
version: 2.1.1
|
||||
version: 2.1.2
|
||||
|
||||
servers:
|
||||
- url: /api/v1
|
||||
|
@ -965,6 +965,10 @@ components:
|
|||
enum:
|
||||
- Plain
|
||||
- AES-256-GCM
|
||||
- Secretbox
|
||||
- GCP
|
||||
- AWS
|
||||
- VaultTransit
|
||||
- Redacted
|
||||
description: Set to "Plain" to add or update an existing secret, set to "Redacted" to preserve the existing value
|
||||
payload:
|
||||
|
@ -1234,7 +1238,7 @@ components:
|
|||
last_login:
|
||||
type: integer
|
||||
format: int64
|
||||
description: Last user login as unix timestamp in milliseconds
|
||||
description: Last user login as unix timestamp in milliseconds. It is saved at most once every 10 minutes
|
||||
filters:
|
||||
$ref: '#/components/schemas/UserFilters'
|
||||
filesystem:
|
||||
|
|
24
httpd/web.go
24
httpd/web.go
|
@ -16,6 +16,7 @@ import (
|
|||
|
||||
"github.com/drakkan/sftpgo/common"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/drakkan/sftpgo/version"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
|
@ -205,6 +206,7 @@ func renderNotFoundPage(w http.ResponseWriter, err error) {
|
|||
}
|
||||
|
||||
func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
|
||||
user.SetEmptySecretsIfNil()
|
||||
data := userPage{
|
||||
basePage: getBasePageData("Add a new user", webUserPath),
|
||||
IsAdd: true,
|
||||
|
@ -222,6 +224,7 @@ func renderAddUserPage(w http.ResponseWriter, user dataprovider.User, error stri
|
|||
}
|
||||
|
||||
func renderUpdateUserPage(w http.ResponseWriter, user dataprovider.User, error string) {
|
||||
user.SetEmptySecretsIfNil()
|
||||
data := userPage{
|
||||
basePage: getBasePageData("Update user", fmt.Sprintf("%v/%v", webUserPath, user.ID)),
|
||||
IsAdd: false,
|
||||
|
@ -430,16 +433,13 @@ func getFiltersFromUserPostFields(r *http.Request) dataprovider.UserFilters {
|
|||
return filters
|
||||
}
|
||||
|
||||
func getSecretFromFormField(r *http.Request, field string) vfs.Secret {
|
||||
secret := vfs.Secret{
|
||||
Payload: r.Form.Get(field),
|
||||
Status: vfs.SecretStatusPlain,
|
||||
func getSecretFromFormField(r *http.Request, field string) *kms.Secret {
|
||||
secret := kms.NewPlainSecret(r.Form.Get(field))
|
||||
if strings.TrimSpace(secret.GetPayload()) == redactedSecret {
|
||||
secret.SetStatus(kms.SecretStatusRedacted)
|
||||
}
|
||||
if strings.TrimSpace(secret.Payload) == redactedSecret {
|
||||
secret.Status = vfs.SecretStatusRedacted
|
||||
}
|
||||
if strings.TrimSpace(secret.Payload) == "" {
|
||||
secret.Status = ""
|
||||
if strings.TrimSpace(secret.GetPayload()) == "" {
|
||||
secret.SetStatus("")
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
@ -492,10 +492,7 @@ func getFsConfigFromUserPostFields(r *http.Request) (dataprovider.Filesystem, er
|
|||
}
|
||||
return fs, err
|
||||
}
|
||||
fs.GCSConfig.Credentials = vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: string(fileBytes),
|
||||
}
|
||||
fs.GCSConfig.Credentials = kms.NewPlainSecret(string(fileBytes))
|
||||
fs.GCSConfig.AutomaticCredentials = 0
|
||||
} else if fs.Provider == dataprovider.AzureBlobFilesystemProvider {
|
||||
fs.AzBlobConfig.Container = r.Form.Get("az_container")
|
||||
|
@ -680,6 +677,7 @@ func handleWebUpdateUserPost(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
updatedUser.ID = user.ID
|
||||
updatedUser.SetEmptySecretsIfNil()
|
||||
if len(updatedUser.Password) == 0 {
|
||||
updatedUser.Password = user.Password
|
||||
}
|
||||
|
|
42
kms/aws.go
Normal file
42
kms/aws.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package kms
|
||||
|
||||
const (
|
||||
awsProviderName = "AWS"
|
||||
)
|
||||
|
||||
type awsSecret struct {
|
||||
baseGCloudSecret
|
||||
}
|
||||
|
||||
func newAWSSecret(base baseSecret, url, masterKey string) SecretProvider {
|
||||
return &awsSecret{
|
||||
baseGCloudSecret{
|
||||
baseSecret: base,
|
||||
url: url,
|
||||
masterKey: masterKey,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *awsSecret) Name() string {
|
||||
return awsProviderName
|
||||
}
|
||||
|
||||
func (s *awsSecret) IsEncrypted() bool {
|
||||
return s.Status == SecretStatusAWS
|
||||
}
|
||||
|
||||
func (s *awsSecret) Encrypt() error {
|
||||
if err := s.baseGCloudSecret.Encrypt(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Status = SecretStatusAWS
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *awsSecret) Decrypt() error {
|
||||
if !s.IsEncrypted() {
|
||||
return errWrongSecretStatus
|
||||
}
|
||||
return s.baseGCloudSecret.Decrypt()
|
||||
}
|
99
kms/basegocloud.go
Normal file
99
kms/basegocloud.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package kms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"gocloud.dev/secrets"
|
||||
// import awskms package
|
||||
_ "gocloud.dev/secrets/awskms"
|
||||
// import gcpkms package
|
||||
_ "gocloud.dev/secrets/gcpkms"
|
||||
// import hashivault package
|
||||
_ "gocloud.dev/secrets/hashivault"
|
||||
)
|
||||
|
||||
type baseGCloudSecret struct {
|
||||
baseSecret
|
||||
masterKey string
|
||||
url string
|
||||
}
|
||||
|
||||
func (s *baseGCloudSecret) Encrypt() error {
|
||||
if s.Status != SecretStatusPlain {
|
||||
return errWrongSecretStatus
|
||||
}
|
||||
if s.Payload == "" {
|
||||
return errInvalidSecret
|
||||
}
|
||||
|
||||
payload := s.Payload
|
||||
key := ""
|
||||
if s.masterKey != "" {
|
||||
localSecret := newLocalSecret(s.baseSecret, s.masterKey)
|
||||
err := localSecret.Encrypt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload = localSecret.GetPayload()
|
||||
key = localSecret.GetKey()
|
||||
}
|
||||
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(defaultTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
keeper, err := secrets.OpenKeeper(ctx, s.url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer keeper.Close()
|
||||
ciphertext, err := keeper.Encrypt(context.Background(), []byte(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Payload = base64.StdEncoding.EncodeToString(ciphertext)
|
||||
s.Key = key
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *baseGCloudSecret) Decrypt() error {
|
||||
encrypted, err := base64.StdEncoding.DecodeString(s.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancelFn := context.WithDeadline(context.Background(), time.Now().Add(defaultTimeout))
|
||||
defer cancelFn()
|
||||
|
||||
keeper, err := secrets.OpenKeeper(ctx, s.url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer keeper.Close()
|
||||
plaintext, err := keeper.Decrypt(context.Background(), encrypted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := string(plaintext)
|
||||
if s.Key != "" {
|
||||
baseSecret := baseSecret{
|
||||
Status: SecretStatusSecretBox,
|
||||
Payload: string(plaintext),
|
||||
Key: s.Key,
|
||||
AdditionalData: s.AdditionalData,
|
||||
}
|
||||
localSecret := newLocalSecret(baseSecret, s.masterKey)
|
||||
err = localSecret.Decrypt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload = localSecret.GetPayload()
|
||||
}
|
||||
s.Status = SecretStatusPlain
|
||||
s.Payload = payload
|
||||
s.Key = ""
|
||||
s.AdditionalData = ""
|
||||
return nil
|
||||
}
|
53
kms/basesecret.go
Normal file
53
kms/basesecret.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package kms
|
||||
|
||||
// baseSecret defines the base struct shared among all the secret providers
|
||||
type baseSecret struct {
|
||||
Status SecretStatus `json:"status,omitempty"`
|
||||
Payload string `json:"payload,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
AdditionalData string `json:"additional_data,omitempty"`
|
||||
}
|
||||
|
||||
func (s *baseSecret) GetStatus() SecretStatus {
|
||||
return s.Status
|
||||
}
|
||||
|
||||
func (s *baseSecret) GetPayload() string {
|
||||
return s.Payload
|
||||
}
|
||||
|
||||
func (s *baseSecret) GetKey() string {
|
||||
return s.Key
|
||||
}
|
||||
|
||||
func (s *baseSecret) GetAdditionalData() string {
|
||||
return s.AdditionalData
|
||||
}
|
||||
|
||||
func (s *baseSecret) SetKey(value string) {
|
||||
s.Key = value
|
||||
}
|
||||
|
||||
func (s *baseSecret) SetAdditionalData(value string) {
|
||||
s.AdditionalData = value
|
||||
}
|
||||
|
||||
func (s *baseSecret) SetStatus(value SecretStatus) {
|
||||
s.Status = value
|
||||
}
|
||||
|
||||
func (s *baseSecret) isEmpty() bool {
|
||||
if s.Status != "" {
|
||||
return false
|
||||
}
|
||||
if s.Payload != "" {
|
||||
return false
|
||||
}
|
||||
if s.Key != "" {
|
||||
return false
|
||||
}
|
||||
if s.AdditionalData != "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
122
kms/builtin.go
Normal file
122
kms/builtin.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package kms
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const (
|
||||
builtinProviderName = "Builtin"
|
||||
)
|
||||
|
||||
type builtinSecret struct {
|
||||
baseSecret
|
||||
}
|
||||
|
||||
func newBuiltinSecret(base baseSecret) SecretProvider {
|
||||
return &builtinSecret{
|
||||
baseSecret: base,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *builtinSecret) Name() string {
|
||||
return builtinProviderName
|
||||
}
|
||||
|
||||
func (s *builtinSecret) IsEncrypted() bool {
|
||||
return s.Status == SecretStatusAES256GCM
|
||||
}
|
||||
|
||||
func (s *builtinSecret) deriveKey(key []byte) []byte {
|
||||
var combined []byte
|
||||
combined = append(combined, key...)
|
||||
if s.AdditionalData != "" {
|
||||
combined = append(combined, []byte(s.AdditionalData)...)
|
||||
}
|
||||
combined = append(combined, key...)
|
||||
hash := sha256.Sum256(combined)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
func (s *builtinSecret) Encrypt() error {
|
||||
if s.Payload == "" {
|
||||
return errInvalidSecret
|
||||
}
|
||||
switch s.Status {
|
||||
case SecretStatusPlain:
|
||||
key := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return err
|
||||
}
|
||||
block, err := aes.NewCipher(s.deriveKey(key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return err
|
||||
}
|
||||
var aad []byte
|
||||
if s.AdditionalData != "" {
|
||||
aad = []byte(s.AdditionalData)
|
||||
}
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(s.Payload), aad)
|
||||
s.Key = hex.EncodeToString(key)
|
||||
s.Payload = hex.EncodeToString(ciphertext)
|
||||
s.Status = SecretStatusAES256GCM
|
||||
return nil
|
||||
default:
|
||||
return errWrongSecretStatus
|
||||
}
|
||||
}
|
||||
|
||||
func (s *builtinSecret) Decrypt() error {
|
||||
switch s.Status {
|
||||
case SecretStatusAES256GCM:
|
||||
encrypted, err := hex.DecodeString(s.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := hex.DecodeString(s.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block, err := aes.NewCipher(s.deriveKey(key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(encrypted) < nonceSize {
|
||||
return errMalformedCiphertext
|
||||
}
|
||||
nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:]
|
||||
var aad []byte
|
||||
if s.AdditionalData != "" {
|
||||
aad = []byte(s.AdditionalData)
|
||||
}
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, aad)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Status = SecretStatusPlain
|
||||
s.Payload = string(plaintext)
|
||||
s.Key = ""
|
||||
s.AdditionalData = ""
|
||||
return nil
|
||||
default:
|
||||
return errWrongSecretStatus
|
||||
}
|
||||
}
|
42
kms/gcp.go
Normal file
42
kms/gcp.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package kms
|
||||
|
||||
const (
|
||||
gcpProviderName = "GCP"
|
||||
)
|
||||
|
||||
type gcpSecret struct {
|
||||
baseGCloudSecret
|
||||
}
|
||||
|
||||
func newGCPSecret(base baseSecret, url, masterKey string) SecretProvider {
|
||||
return &gcpSecret{
|
||||
baseGCloudSecret{
|
||||
baseSecret: base,
|
||||
url: url,
|
||||
masterKey: masterKey,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *gcpSecret) Name() string {
|
||||
return gcpProviderName
|
||||
}
|
||||
|
||||
func (s *gcpSecret) IsEncrypted() bool {
|
||||
return s.Status == SecretStatusGCP
|
||||
}
|
||||
|
||||
func (s *gcpSecret) Encrypt() error {
|
||||
if err := s.baseGCloudSecret.Encrypt(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Status = SecretStatusGCP
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *gcpSecret) Decrypt() error {
|
||||
if !s.IsEncrypted() {
|
||||
return errWrongSecretStatus
|
||||
}
|
||||
return s.baseGCloudSecret.Decrypt()
|
||||
}
|
327
kms/kms.go
Normal file
327
kms/kms.go
Normal file
|
@ -0,0 +1,327 @@
|
|||
package kms
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
||||
// SecretProvider defines the interface for a KMS secrets provider
|
||||
type SecretProvider interface {
|
||||
Name() string
|
||||
Encrypt() error
|
||||
Decrypt() error
|
||||
IsEncrypted() bool
|
||||
GetStatus() SecretStatus
|
||||
GetPayload() string
|
||||
GetKey() string
|
||||
GetAdditionalData() string
|
||||
SetKey(string)
|
||||
SetAdditionalData(string)
|
||||
SetStatus(SecretStatus)
|
||||
}
|
||||
|
||||
// SecretStatus defines the statuses of a Secret object
|
||||
type SecretStatus = string
|
||||
|
||||
const (
|
||||
// SecretStatusPlain means the secret is in plain text and must be encrypted
|
||||
SecretStatusPlain SecretStatus = "Plain"
|
||||
// SecretStatusAES256GCM means the secret is encrypted using AES-256-GCM
|
||||
SecretStatusAES256GCM SecretStatus = "AES-256-GCM"
|
||||
// SecretStatusSecretBox means the secret is encrypted using a locally provided symmetric key
|
||||
SecretStatusSecretBox SecretStatus = "Secretbox"
|
||||
// SecretStatusGCP means we use keys from Google Cloud Platform’s Key Management Service
|
||||
// (GCP KMS) to keep information secret
|
||||
SecretStatusGCP SecretStatus = "GCP"
|
||||
// SecretStatusAWS means we use customer master keys from Amazon Web Service’s
|
||||
// Key Management Service (AWS KMS) to keep information secret
|
||||
SecretStatusAWS SecretStatus = "AWS"
|
||||
// SecretStatusVaultTransit means we use the transit secrets engine in Vault
|
||||
// to keep information secret
|
||||
SecretStatusVaultTransit SecretStatus = "VaultTransit"
|
||||
// SecretStatusRedacted means the secret is redacted
|
||||
SecretStatusRedacted SecretStatus = "Redacted"
|
||||
)
|
||||
|
||||
// Configuration defines the KMS configuration
|
||||
type Configuration struct {
|
||||
Secrets Secrets `json:"secrets" mapstructure:"secrets"`
|
||||
}
|
||||
|
||||
// Secrets define the KMS configuration for encryption/decryption
|
||||
type Secrets struct {
|
||||
URL string `json:"url" mapstructure:"url"`
|
||||
MasterKeyPath string `json:"master_key_path" mapstructure:"master_key_path"`
|
||||
masterKey string
|
||||
}
|
||||
|
||||
var (
|
||||
errWrongSecretStatus = errors.New("wrong secret status")
|
||||
errMalformedCiphertext = errors.New("malformed ciphertext")
|
||||
errInvalidSecret = errors.New("invalid secret")
|
||||
validSecretStatuses = []string{SecretStatusPlain, SecretStatusAES256GCM, SecretStatusSecretBox,
|
||||
SecretStatusVaultTransit, SecretStatusAWS, SecretStatusGCP, SecretStatusRedacted}
|
||||
config Configuration
|
||||
defaultTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
// NewSecret builds a new Secret using the provided arguments
|
||||
func NewSecret(status SecretStatus, payload, key, data string) *Secret {
|
||||
return config.newSecret(status, payload, key, data)
|
||||
}
|
||||
|
||||
// NewEmptySecret returns an empty secret
|
||||
func NewEmptySecret() *Secret {
|
||||
return NewSecret("", "", "", "")
|
||||
}
|
||||
|
||||
// NewPlainSecret stores the give payload in a plain text secret
|
||||
func NewPlainSecret(payload string) *Secret {
|
||||
return NewSecret(SecretStatusPlain, payload, "", "")
|
||||
}
|
||||
|
||||
// GetSecretFromCompatString returns a secret from the previous format
|
||||
func GetSecretFromCompatString(secret string) (*Secret, error) {
|
||||
plain, err := utils.DecryptData(secret)
|
||||
if err != nil {
|
||||
return &Secret{}, errMalformedCiphertext
|
||||
}
|
||||
return NewSecret(SecretStatusPlain, plain, "", ""), nil
|
||||
}
|
||||
|
||||
// Initialize configures the KMS support
|
||||
func (c *Configuration) Initialize() error {
|
||||
if c.Secrets.MasterKeyPath != "" {
|
||||
mKey, err := ioutil.ReadFile(c.Secrets.MasterKeyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Secrets.masterKey = strings.TrimSpace(string(mKey))
|
||||
}
|
||||
config = *c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Configuration) newSecret(status SecretStatus, payload, key, data string) *Secret {
|
||||
base := baseSecret{
|
||||
Status: status,
|
||||
Key: key,
|
||||
Payload: payload,
|
||||
AdditionalData: data,
|
||||
}
|
||||
return &Secret{
|
||||
provider: c.getSecretProvider(base),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Configuration) getSecretProvider(base baseSecret) SecretProvider {
|
||||
if strings.HasPrefix(c.Secrets.URL, "hashivault://") {
|
||||
return newVaultSecret(base, c.Secrets.URL, c.Secrets.masterKey)
|
||||
}
|
||||
if strings.HasPrefix(c.Secrets.URL, "awskms://") {
|
||||
return newAWSSecret(base, c.Secrets.URL, c.Secrets.masterKey)
|
||||
}
|
||||
if strings.HasPrefix(c.Secrets.URL, "gcpkms://") {
|
||||
return newGCPSecret(base, c.Secrets.URL, c.Secrets.masterKey)
|
||||
}
|
||||
return newLocalSecret(base, c.Secrets.masterKey)
|
||||
}
|
||||
|
||||
// Secret defines the struct used to store confidential data
|
||||
type Secret struct {
|
||||
provider SecretProvider
|
||||
}
|
||||
|
||||
// MarshalJSON return the JSON encoding of the Secret object
|
||||
func (s *Secret) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&baseSecret{
|
||||
Status: s.provider.GetStatus(),
|
||||
Payload: s.provider.GetPayload(),
|
||||
Key: s.provider.GetKey(),
|
||||
AdditionalData: s.provider.GetAdditionalData(),
|
||||
})
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the JSON-encoded data and stores the result
|
||||
// in the Secret object
|
||||
func (s *Secret) UnmarshalJSON(data []byte) error {
|
||||
baseSecret := baseSecret{}
|
||||
err := json.Unmarshal(data, &baseSecret)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if baseSecret.isEmpty() {
|
||||
s.provider = config.getSecretProvider(baseSecret)
|
||||
return nil
|
||||
}
|
||||
switch baseSecret.Status {
|
||||
case SecretStatusAES256GCM:
|
||||
s.provider = newBuiltinSecret(baseSecret)
|
||||
case SecretStatusSecretBox:
|
||||
s.provider = newLocalSecret(baseSecret, config.Secrets.masterKey)
|
||||
case SecretStatusVaultTransit:
|
||||
s.provider = newVaultSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey)
|
||||
case SecretStatusAWS:
|
||||
s.provider = newAWSSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey)
|
||||
case SecretStatusGCP:
|
||||
s.provider = newGCPSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey)
|
||||
case SecretStatusPlain, SecretStatusRedacted:
|
||||
s.provider = config.getSecretProvider(baseSecret)
|
||||
default:
|
||||
return errInvalidSecret
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone returns a copy of the secret object
|
||||
func (s *Secret) Clone() *Secret {
|
||||
baseSecret := baseSecret{
|
||||
Status: s.provider.GetStatus(),
|
||||
Payload: s.provider.GetPayload(),
|
||||
Key: s.provider.GetKey(),
|
||||
AdditionalData: s.provider.GetAdditionalData(),
|
||||
}
|
||||
switch s.provider.Name() {
|
||||
case builtinProviderName:
|
||||
return &Secret{
|
||||
provider: newBuiltinSecret(baseSecret),
|
||||
}
|
||||
case awsProviderName:
|
||||
return &Secret{
|
||||
provider: newAWSSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey),
|
||||
}
|
||||
case gcpProviderName:
|
||||
return &Secret{
|
||||
provider: newGCPSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey),
|
||||
}
|
||||
case localProviderName:
|
||||
return &Secret{
|
||||
provider: newLocalSecret(baseSecret, config.Secrets.masterKey),
|
||||
}
|
||||
case vaultProviderName:
|
||||
return &Secret{
|
||||
provider: newVaultSecret(baseSecret, config.Secrets.URL, config.Secrets.masterKey),
|
||||
}
|
||||
}
|
||||
return NewSecret(s.GetStatus(), s.GetPayload(), s.GetKey(), s.GetAdditionalData())
|
||||
}
|
||||
|
||||
// IsEncrypted returns true if the secret is encrypted
|
||||
// This isn't a pointer receiver because we don't want to pass
|
||||
// a pointer to html template
|
||||
func (s *Secret) IsEncrypted() bool {
|
||||
return s.provider.IsEncrypted()
|
||||
}
|
||||
|
||||
// IsPlain returns true if the secret is in plain text
|
||||
func (s *Secret) IsPlain() bool {
|
||||
return s.provider.GetStatus() == SecretStatusPlain
|
||||
}
|
||||
|
||||
// IsRedacted returns true if the secret is redacted
|
||||
func (s *Secret) IsRedacted() bool {
|
||||
return s.provider.GetStatus() == SecretStatusRedacted
|
||||
}
|
||||
|
||||
// GetPayload returns the secret payload
|
||||
func (s *Secret) GetPayload() string {
|
||||
return s.provider.GetPayload()
|
||||
}
|
||||
|
||||
// GetAdditionalData returns the secret additional data
|
||||
func (s *Secret) GetAdditionalData() string {
|
||||
return s.provider.GetAdditionalData()
|
||||
}
|
||||
|
||||
// GetStatus returns the secret status
|
||||
func (s *Secret) GetStatus() SecretStatus {
|
||||
return s.provider.GetStatus()
|
||||
}
|
||||
|
||||
// GetKey returns the secret key
|
||||
func (s *Secret) GetKey() string {
|
||||
return s.provider.GetKey()
|
||||
}
|
||||
|
||||
// SetAdditionalData sets the given additional data
|
||||
func (s *Secret) SetAdditionalData(value string) {
|
||||
s.provider.SetAdditionalData(value)
|
||||
}
|
||||
|
||||
// SetStatus sets the status for this secret
|
||||
func (s *Secret) SetStatus(value SecretStatus) {
|
||||
s.provider.SetStatus(value)
|
||||
}
|
||||
|
||||
// SetKey sets the key for this secret
|
||||
func (s *Secret) SetKey(value string) {
|
||||
s.provider.SetKey(value)
|
||||
}
|
||||
|
||||
// IsEmpty returns true if all fields are empty
|
||||
func (s *Secret) IsEmpty() bool {
|
||||
if s.provider.GetStatus() != "" {
|
||||
return false
|
||||
}
|
||||
if s.provider.GetPayload() != "" {
|
||||
return false
|
||||
}
|
||||
if s.provider.GetKey() != "" {
|
||||
return false
|
||||
}
|
||||
if s.provider.GetAdditionalData() != "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValid returns true if the secret is not empty and valid
|
||||
func (s *Secret) IsValid() bool {
|
||||
if !s.IsValidInput() {
|
||||
return false
|
||||
}
|
||||
switch s.provider.GetStatus() {
|
||||
case SecretStatusAES256GCM, SecretStatusSecretBox:
|
||||
if len(s.provider.GetKey()) != 64 {
|
||||
return false
|
||||
}
|
||||
case SecretStatusAWS, SecretStatusGCP, SecretStatusVaultTransit:
|
||||
key := s.provider.GetKey()
|
||||
if key != "" && len(key) != 64 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValidInput returns true if the secret is a valid user input
|
||||
func (s *Secret) IsValidInput() bool {
|
||||
if !utils.IsStringInSlice(s.provider.GetStatus(), validSecretStatuses) {
|
||||
return false
|
||||
}
|
||||
if s.provider.GetPayload() == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Hide hides info to decrypt data
|
||||
func (s *Secret) Hide() {
|
||||
s.provider.SetKey("")
|
||||
s.provider.SetAdditionalData("")
|
||||
}
|
||||
|
||||
// Encrypt encrypts a plain text Secret object
|
||||
func (s *Secret) Encrypt() error {
|
||||
return s.provider.Encrypt()
|
||||
}
|
||||
|
||||
// Decrypt decrypts a Secret object
|
||||
func (s *Secret) Decrypt() error {
|
||||
return s.provider.Decrypt()
|
||||
}
|
120
kms/local.go
Normal file
120
kms/local.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package kms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
"gocloud.dev/secrets/localsecrets"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
const (
|
||||
localProviderName = "Local"
|
||||
)
|
||||
|
||||
type localSecret struct {
|
||||
baseSecret
|
||||
masterKey string
|
||||
}
|
||||
|
||||
func newLocalSecret(base baseSecret, masterKey string) SecretProvider {
|
||||
return &localSecret{
|
||||
baseSecret: base,
|
||||
masterKey: masterKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *localSecret) Name() string {
|
||||
return localProviderName
|
||||
}
|
||||
|
||||
func (s *localSecret) IsEncrypted() bool {
|
||||
return s.Status == SecretStatusSecretBox
|
||||
}
|
||||
|
||||
func (s *localSecret) Encrypt() error {
|
||||
if s.Status != SecretStatusPlain {
|
||||
return errWrongSecretStatus
|
||||
}
|
||||
if s.Payload == "" {
|
||||
return errInvalidSecret
|
||||
}
|
||||
secretKey, err := localsecrets.NewRandomKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := s.deriveKey(secretKey[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keeper := localsecrets.NewKeeper(key)
|
||||
defer keeper.Close()
|
||||
|
||||
ciphertext, err := keeper.Encrypt(context.Background(), []byte(s.Payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Key = hex.EncodeToString(secretKey[:])
|
||||
s.Payload = base64.StdEncoding.EncodeToString(ciphertext)
|
||||
s.Status = SecretStatusSecretBox
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *localSecret) Decrypt() error {
|
||||
if !s.IsEncrypted() {
|
||||
return errWrongSecretStatus
|
||||
}
|
||||
encrypted, err := base64.StdEncoding.DecodeString(s.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secretKey, err := hex.DecodeString(s.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := s.deriveKey(secretKey[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keeper := localsecrets.NewKeeper(key)
|
||||
defer keeper.Close()
|
||||
|
||||
plaintext, err := keeper.Decrypt(context.Background(), encrypted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Status = SecretStatusPlain
|
||||
s.Payload = string(plaintext)
|
||||
s.Key = ""
|
||||
s.AdditionalData = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *localSecret) deriveKey(key []byte) ([32]byte, error) {
|
||||
var masterKey []byte
|
||||
if s.masterKey == "" {
|
||||
var combined []byte
|
||||
combined = append(combined, key...)
|
||||
if s.AdditionalData != "" {
|
||||
combined = append(combined, []byte(s.AdditionalData)...)
|
||||
}
|
||||
combined = append(combined, key...)
|
||||
hash := sha256.Sum256(combined)
|
||||
masterKey = hash[:]
|
||||
} else {
|
||||
masterKey = []byte(s.masterKey)
|
||||
}
|
||||
var derivedKey [32]byte
|
||||
var info []byte
|
||||
if s.AdditionalData != "" {
|
||||
info = []byte(s.AdditionalData)
|
||||
}
|
||||
kdf := hkdf.New(sha256.New, masterKey, key, info)
|
||||
if _, err := io.ReadFull(kdf, derivedKey[:]); err != nil {
|
||||
return derivedKey, err
|
||||
}
|
||||
return derivedKey, nil
|
||||
}
|
42
kms/vault.go
Normal file
42
kms/vault.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
package kms
|
||||
|
||||
const (
|
||||
vaultProviderName = "VaultTransit"
|
||||
)
|
||||
|
||||
type vaultSecret struct {
|
||||
baseGCloudSecret
|
||||
}
|
||||
|
||||
func newVaultSecret(base baseSecret, url, masterKey string) SecretProvider {
|
||||
return &vaultSecret{
|
||||
baseGCloudSecret{
|
||||
baseSecret: base,
|
||||
url: url,
|
||||
masterKey: masterKey,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *vaultSecret) Name() string {
|
||||
return vaultProviderName
|
||||
}
|
||||
|
||||
func (s *vaultSecret) IsEncrypted() bool {
|
||||
return s.Status == SecretStatusVaultTransit
|
||||
}
|
||||
|
||||
func (s *vaultSecret) Encrypt() error {
|
||||
if err := s.baseGCloudSecret.Encrypt(); err != nil {
|
||||
return err
|
||||
}
|
||||
s.Status = SecretStatusVaultTransit
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *vaultSecret) Decrypt() error {
|
||||
if !s.IsEncrypted() {
|
||||
return errWrongSecretStatus
|
||||
}
|
||||
return s.baseGCloudSecret.Decrypt()
|
||||
}
|
|
@ -109,6 +109,13 @@ func (s *Service) Start() error {
|
|||
|
||||
httpConfig := config.GetHTTPConfig()
|
||||
httpConfig.Initialize(s.ConfigDir)
|
||||
kmsConfig := config.GetKMSConfig()
|
||||
err = kmsConfig.Initialize()
|
||||
if err != nil {
|
||||
logger.Error(logSender, "", "unable to initialize KMS: %v", err)
|
||||
logger.ErrorToConsole("unable to initialize KMS: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
s.startServices()
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
"github.com/drakkan/sftpgo/config"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/sftpd"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
|
@ -32,6 +33,11 @@ func (s *Service) StartPortableMode(sftpdPort, ftpPort, webdavPort int, enabledS
|
|||
if err != nil {
|
||||
fmt.Printf("error loading configuration file: %v using defaults\n", err)
|
||||
}
|
||||
kmsConfig := config.GetKMSConfig()
|
||||
err = kmsConfig.Initialize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printablePassword := s.configurePortableUser()
|
||||
dataProviderConf := config.GetProviderConf()
|
||||
dataProviderConf.Driver = dataprovider.MemoryDataProviderName
|
||||
|
@ -237,5 +243,23 @@ func (s *Service) configurePortableUser() string {
|
|||
s.PortableUser.Password = b.String()
|
||||
printablePassword = s.PortableUser.Password
|
||||
}
|
||||
// we created the user before to initialize the KMS so we need to create the secret here
|
||||
switch s.PortableUser.FsConfig.Provider {
|
||||
case dataprovider.S3FilesystemProvider:
|
||||
payload := s.PortableUser.FsConfig.S3Config.AccessSecret.GetPayload()
|
||||
if payload != "" {
|
||||
s.PortableUser.FsConfig.S3Config.AccessSecret = kms.NewPlainSecret(payload)
|
||||
}
|
||||
case dataprovider.GCSFilesystemProvider:
|
||||
payload := s.PortableUser.FsConfig.GCSConfig.Credentials.GetPayload()
|
||||
if payload != "" {
|
||||
s.PortableUser.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(payload)
|
||||
}
|
||||
case dataprovider.AzureBlobFilesystemProvider:
|
||||
payload := s.PortableUser.FsConfig.AzBlobConfig.AccountKey.GetPayload()
|
||||
if payload != "" {
|
||||
s.PortableUser.FsConfig.AzBlobConfig.AccountKey = kms.NewPlainSecret(payload)
|
||||
}
|
||||
}
|
||||
return printablePassword
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/config"
|
||||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
|
@ -177,6 +178,12 @@ func TestMain(m *testing.M) {
|
|||
|
||||
httpConfig := config.GetHTTPConfig()
|
||||
httpConfig.Initialize(configDir)
|
||||
kmsConfig := config.GetKMSConfig()
|
||||
err = kmsConfig.Initialize()
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("error initializing kms: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
sftpdConf := config.GetSFTPDConfig()
|
||||
httpdConf := config.GetHTTPDConfig()
|
||||
|
@ -1312,10 +1319,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
u := getTestUser(usePubKey)
|
||||
u.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
u.FsConfig.GCSConfig.Bucket = "testbucket"
|
||||
u.FsConfig.GCSConfig.Credentials = vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: `{ "type": "service_account" }`,
|
||||
}
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ "type": "service_account" }`)
|
||||
|
||||
providerConf := config.GetProviderConf()
|
||||
providerConf.PreferDatabaseCredentials = true
|
||||
|
@ -1336,10 +1340,10 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.GCSConfig.Credentials.Status)
|
||||
assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.Payload)
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.AdditionalData)
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.Key)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())
|
||||
assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload())
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetKey())
|
||||
|
||||
assert.NoFileExists(t, credentialsFile)
|
||||
|
||||
|
@ -1364,10 +1368,7 @@ func TestLoginInvalidFs(t *testing.T) {
|
|||
u := getTestUser(usePubKey)
|
||||
u.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
u.FsConfig.GCSConfig.Bucket = "test"
|
||||
u.FsConfig.GCSConfig.Credentials = vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: "invalid JSON for credentials",
|
||||
}
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
|
@ -122,5 +122,11 @@
|
|||
"timeout": 20,
|
||||
"ca_certificates": [],
|
||||
"skip_tls_verify": false
|
||||
},
|
||||
"kms": {
|
||||
"secrets": {
|
||||
"url": "",
|
||||
"master_key_path": ""
|
||||
}
|
||||
}
|
||||
}
|
|
@ -337,7 +337,7 @@
|
|||
<label for="idS3AccessSecret" class="col-sm-2 col-form-label">Access Secret</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" id="idS3AccessSecret" name="s3_access_secret" placeholder=""
|
||||
value="{{if .IsS3SecretEnc}}{{.RedactedSecret}}{{else}}{{.User.FsConfig.S3Config.AccessSecret.Payload}}{{end}}" maxlength="1000">
|
||||
value="{{if .IsS3SecretEnc}}{{.RedactedSecret}}{{else}}{{.User.FsConfig.S3Config.AccessSecret.GetPayload}}{{end}}" maxlength="1000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -448,7 +448,7 @@
|
|||
<label for="idAzAccountKey" class="col-sm-2 col-form-label">Account Key</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idAzAccountKey" name="az_account_key" placeholder=""
|
||||
value="{{if .IsAzSecretEnc}}{{.RedactedSecret}}{{else}}{{.User.FsConfig.AzBlobConfig.AccountKey.Payload}}{{end}}" maxlength="1000">
|
||||
value="{{if .IsAzSecretEnc}}{{.RedactedSecret}}{{else}}{{.User.FsConfig.AzBlobConfig.AccountKey.GetPayload}}{{end}}" maxlength="1000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -104,7 +104,7 @@ func NewAzBlobFs(connectionID, localTempDir string, config AzBlobFsConfig) (Fs,
|
|||
return fs, nil
|
||||
}
|
||||
|
||||
credential, err := azblob.NewSharedKeyCredential(fs.config.AccountName, fs.config.AccountKey.Payload)
|
||||
credential, err := azblob.NewSharedKeyCredential(fs.config.AccountName, fs.config.AccountKey.GetPayload())
|
||||
if err != nil {
|
||||
return fs, fmt.Errorf("invalid credentials: %v", err)
|
||||
}
|
||||
|
|
17
vfs/gcsfs.go
17
vfs/gcsfs.go
|
@ -23,6 +23,7 @@ import (
|
|||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/metrics"
|
||||
"github.com/drakkan/sftpgo/version"
|
||||
|
@ -62,19 +63,21 @@ func NewGCSFs(connectionID, localTempDir string, config GCSFsConfig) (Fs, error)
|
|||
ctx := context.Background()
|
||||
if fs.config.AutomaticCredentials > 0 {
|
||||
fs.svc, err = storage.NewClient(ctx)
|
||||
} else if fs.config.Credentials.IsEncrypted() {
|
||||
err = fs.config.Credentials.Decrypt()
|
||||
if err != nil {
|
||||
return fs, err
|
||||
} else if !fs.config.Credentials.IsEmpty() {
|
||||
if fs.config.Credentials.IsEncrypted() {
|
||||
err = fs.config.Credentials.Decrypt()
|
||||
if err != nil {
|
||||
return fs, err
|
||||
}
|
||||
}
|
||||
fs.svc, err = storage.NewClient(ctx, option.WithCredentialsJSON([]byte(fs.config.Credentials.Payload)))
|
||||
fs.svc, err = storage.NewClient(ctx, option.WithCredentialsJSON([]byte(fs.config.Credentials.GetPayload())))
|
||||
} else {
|
||||
var creds []byte
|
||||
creds, err = ioutil.ReadFile(fs.config.CredentialFile)
|
||||
if err != nil {
|
||||
return fs, err
|
||||
}
|
||||
secret := &Secret{}
|
||||
secret := kms.NewEmptySecret()
|
||||
err = json.Unmarshal(creds, secret)
|
||||
if err != nil {
|
||||
return fs, err
|
||||
|
@ -83,7 +86,7 @@ func NewGCSFs(connectionID, localTempDir string, config GCSFsConfig) (Fs, error)
|
|||
if err != nil {
|
||||
return fs, err
|
||||
}
|
||||
fs.svc, err = storage.NewClient(ctx, option.WithCredentialsJSON([]byte(secret.Payload)))
|
||||
fs.svc, err = storage.NewClient(ctx, option.WithCredentialsJSON([]byte(secret.GetPayload())))
|
||||
}
|
||||
return fs, err
|
||||
}
|
||||
|
|
12
vfs/s3fs.go
12
vfs/s3fs.go
|
@ -60,12 +60,14 @@ func NewS3Fs(connectionID, localTempDir string, config S3FsConfig) (Fs, error) {
|
|||
awsConfig.WithRegion(fs.config.Region)
|
||||
}
|
||||
|
||||
if fs.config.AccessSecret.IsEncrypted() {
|
||||
err := fs.config.AccessSecret.Decrypt()
|
||||
if err != nil {
|
||||
return fs, err
|
||||
if !fs.config.AccessSecret.IsEmpty() {
|
||||
if fs.config.AccessSecret.IsEncrypted() {
|
||||
err := fs.config.AccessSecret.Decrypt()
|
||||
if err != nil {
|
||||
return fs, err
|
||||
}
|
||||
}
|
||||
awsConfig.Credentials = credentials.NewStaticCredentials(fs.config.AccessKey, fs.config.AccessSecret.Payload, "")
|
||||
awsConfig.Credentials = credentials.NewStaticCredentials(fs.config.AccessKey, fs.config.AccessSecret.GetPayload(), "")
|
||||
}
|
||||
|
||||
if fs.config.Endpoint != "" {
|
||||
|
|
209
vfs/secret.go
209
vfs/secret.go
|
@ -1,209 +0,0 @@
|
|||
package vfs
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
||||
// SecretStatus defines the statuses of a Secret object
|
||||
type SecretStatus = string
|
||||
|
||||
const (
|
||||
// SecretStatusPlain means the secret is in plain text and must be encrypted
|
||||
SecretStatusPlain SecretStatus = "Plain"
|
||||
// SecretStatusAES256GCM means the secret is encrypted using AES-256-GCM
|
||||
SecretStatusAES256GCM SecretStatus = "AES-256-GCM"
|
||||
// SecretStatusRedacted means the secret is redacted
|
||||
SecretStatusRedacted SecretStatus = "Redacted"
|
||||
)
|
||||
|
||||
var (
|
||||
errWrongSecretStatus = errors.New("wrong secret status")
|
||||
errMalformedCiphertext = errors.New("malformed ciphertext")
|
||||
errInvalidSecret = errors.New("invalid secret")
|
||||
validSecretStatuses = []string{SecretStatusPlain, SecretStatusAES256GCM, SecretStatusRedacted}
|
||||
)
|
||||
|
||||
// Secret defines the struct used to store confidential data
|
||||
type Secret struct {
|
||||
Status SecretStatus `json:"status,omitempty"`
|
||||
Payload string `json:"payload,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
AdditionalData string `json:"additional_data,omitempty"`
|
||||
}
|
||||
|
||||
// GetSecretFromCompatString returns a secret from the previous format
|
||||
func GetSecretFromCompatString(secret string) (Secret, error) {
|
||||
s := Secret{}
|
||||
plain, err := utils.DecryptData(secret)
|
||||
if err != nil {
|
||||
return s, errMalformedCiphertext
|
||||
}
|
||||
s.Status = SecretStatusPlain
|
||||
s.Payload = plain
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// IsEncrypted returns true if the secret is encrypted
|
||||
// This isn't a pointer receiver because we don't want to pass
|
||||
// a pointer to html template
|
||||
func (s *Secret) IsEncrypted() bool {
|
||||
return s.Status == SecretStatusAES256GCM
|
||||
}
|
||||
|
||||
// IsPlain returns true if the secret is in plain text
|
||||
func (s *Secret) IsPlain() bool {
|
||||
return s.Status == SecretStatusPlain
|
||||
}
|
||||
|
||||
// IsRedacted returns true if the secret is redacted
|
||||
func (s *Secret) IsRedacted() bool {
|
||||
return s.Status == SecretStatusRedacted
|
||||
}
|
||||
|
||||
// IsEmpty returns true if all fields are empty
|
||||
func (s *Secret) IsEmpty() bool {
|
||||
if s.Status != "" {
|
||||
return false
|
||||
}
|
||||
if s.Payload != "" {
|
||||
return false
|
||||
}
|
||||
if s.Key != "" {
|
||||
return false
|
||||
}
|
||||
if s.AdditionalData != "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValid returns true if the secret is not empty and valid
|
||||
func (s *Secret) IsValid() bool {
|
||||
if !s.IsValidInput() {
|
||||
return false
|
||||
}
|
||||
if s.Status == SecretStatusAES256GCM {
|
||||
if len(s.Key) != 64 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// IsValidInput returns true if the secret is a valid user input
|
||||
func (s *Secret) IsValidInput() bool {
|
||||
if !utils.IsStringInSlice(s.Status, validSecretStatuses) {
|
||||
return false
|
||||
}
|
||||
if s.Payload == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Hide hides info to decrypt data
|
||||
func (s *Secret) Hide() {
|
||||
s.Key = ""
|
||||
s.AdditionalData = ""
|
||||
}
|
||||
|
||||
// deriveKey is a weak method of deriving a key but it is still better than using the key as it is.
|
||||
// We should use a KMS in future
|
||||
func (s *Secret) deriveKey(key []byte) []byte {
|
||||
var combined []byte
|
||||
combined = append(combined, key...)
|
||||
if s.AdditionalData != "" {
|
||||
combined = append(combined, []byte(s.AdditionalData)...)
|
||||
}
|
||||
combined = append(combined, key...)
|
||||
hash := sha256.Sum256(combined)
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
// Encrypt encrypts a plain text Secret object
|
||||
func (s *Secret) Encrypt() error {
|
||||
if s.Payload == "" {
|
||||
return errInvalidSecret
|
||||
}
|
||||
switch s.Status {
|
||||
case SecretStatusPlain:
|
||||
key := make([]byte, 32)
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
return err
|
||||
}
|
||||
block, err := aes.NewCipher(s.deriveKey(key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return err
|
||||
}
|
||||
var aad []byte
|
||||
if s.AdditionalData != "" {
|
||||
aad = []byte(s.AdditionalData)
|
||||
}
|
||||
ciphertext := gcm.Seal(nonce, nonce, []byte(s.Payload), aad)
|
||||
s.Key = hex.EncodeToString(key)
|
||||
s.Payload = hex.EncodeToString(ciphertext)
|
||||
s.Status = SecretStatusAES256GCM
|
||||
return nil
|
||||
default:
|
||||
return errWrongSecretStatus
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt decrypts a Secret object
|
||||
func (s *Secret) Decrypt() error {
|
||||
switch s.Status {
|
||||
case SecretStatusAES256GCM:
|
||||
encrypted, err := hex.DecodeString(s.Payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := hex.DecodeString(s.Key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
block, err := aes.NewCipher(s.deriveKey(key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(encrypted) < nonceSize {
|
||||
return errMalformedCiphertext
|
||||
}
|
||||
nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:]
|
||||
var aad []byte
|
||||
if s.AdditionalData != "" {
|
||||
aad = []byte(s.AdditionalData)
|
||||
}
|
||||
plaintext, err := gcm.Open(nil, nonce, ciphertext, aad)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Status = SecretStatusPlain
|
||||
s.Payload = string(plaintext)
|
||||
s.Key = ""
|
||||
s.AdditionalData = ""
|
||||
return nil
|
||||
default:
|
||||
return errWrongSecretStatus
|
||||
}
|
||||
}
|
49
vfs/vfs.go
49
vfs/vfs.go
|
@ -15,6 +15,7 @@ import (
|
|||
|
||||
"github.com/eikenb/pipeat"
|
||||
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/utils"
|
||||
)
|
||||
|
@ -110,12 +111,12 @@ type S3FsConfig struct {
|
|||
// folder. The prefix, if not empty, must not start with "/" and must
|
||||
// end with "/".
|
||||
// If empty the whole bucket contents will be available
|
||||
KeyPrefix string `json:"key_prefix,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret Secret `json:"access_secret,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
StorageClass string `json:"storage_class,omitempty"`
|
||||
KeyPrefix string `json:"key_prefix,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
AccessKey string `json:"access_key,omitempty"`
|
||||
AccessSecret *kms.Secret `json:"access_secret,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
StorageClass string `json:"storage_class,omitempty"`
|
||||
// The buffer size (in MB) to use for multipart uploads. The minimum allowed part size is 5MB,
|
||||
// and if this value is set to zero, the default value (5MB) for the AWS SDK will be used.
|
||||
// The minimum allowed value is 5.
|
||||
|
@ -137,9 +138,9 @@ type GCSFsConfig struct {
|
|||
// folder. The prefix, if not empty, must not start with "/" and must
|
||||
// end with "/".
|
||||
// If empty the whole bucket contents will be available
|
||||
KeyPrefix string `json:"key_prefix,omitempty"`
|
||||
CredentialFile string `json:"-"`
|
||||
Credentials Secret `json:"credentials,omitempty"`
|
||||
KeyPrefix string `json:"key_prefix,omitempty"`
|
||||
CredentialFile string `json:"-"`
|
||||
Credentials *kms.Secret `json:"credentials,omitempty"`
|
||||
// 0 explicit, 1 automatic
|
||||
AutomaticCredentials int `json:"automatic_credentials,omitempty"`
|
||||
StorageClass string `json:"storage_class,omitempty"`
|
||||
|
@ -151,8 +152,8 @@ type AzBlobFsConfig struct {
|
|||
// Storage Account Name, leave blank to use SAS URL
|
||||
AccountName string `json:"account_name,omitempty"`
|
||||
// Storage Account Key leave blank to use SAS URL.
|
||||
// The access key is stored encrypted (AES-256-GCM)
|
||||
AccountKey Secret `json:"account_key,omitempty"`
|
||||
// The access key is stored encrypted based on the kms configuration
|
||||
AccountKey *kms.Secret `json:"account_key,omitempty"`
|
||||
// Optional endpoint. Default is "blob.core.windows.net".
|
||||
// If you use the emulator the endpoint must include the protocol,
|
||||
// for example "http://127.0.0.1:10000"
|
||||
|
@ -254,6 +255,9 @@ func checkS3Credentials(config *S3FsConfig) error {
|
|||
|
||||
// ValidateS3FsConfig returns nil if the specified s3 config is valid, otherwise an error
|
||||
func ValidateS3FsConfig(config *S3FsConfig) error {
|
||||
if config.AccessSecret == nil {
|
||||
config.AccessSecret = kms.NewEmptySecret()
|
||||
}
|
||||
if config.Bucket == "" {
|
||||
return errors.New("bucket cannot be empty")
|
||||
}
|
||||
|
@ -283,6 +287,9 @@ func ValidateS3FsConfig(config *S3FsConfig) error {
|
|||
|
||||
// ValidateGCSFsConfig returns nil if the specified GCS config is valid, otherwise an error
|
||||
func ValidateGCSFsConfig(config *GCSFsConfig, credentialsFilePath string) error {
|
||||
if config.Credentials == nil {
|
||||
config.Credentials = kms.NewEmptySecret()
|
||||
}
|
||||
if config.Bucket == "" {
|
||||
return errors.New("bucket cannot be empty")
|
||||
}
|
||||
|
@ -310,8 +317,21 @@ func ValidateGCSFsConfig(config *GCSFsConfig, credentialsFilePath string) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func checkAzCredentials(config *AzBlobFsConfig) error {
|
||||
if config.AccountName == "" || !config.AccountKey.IsValidInput() {
|
||||
return errors.New("credentials cannot be empty or invalid")
|
||||
}
|
||||
if config.AccountKey.IsEncrypted() && !config.AccountKey.IsValid() {
|
||||
return errors.New("invalid encrypted account_key")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateAzBlobFsConfig returns nil if the specified Azure Blob config is valid, otherwise an error
|
||||
func ValidateAzBlobFsConfig(config *AzBlobFsConfig) error {
|
||||
if config.AccountKey == nil {
|
||||
config.AccountKey = kms.NewEmptySecret()
|
||||
}
|
||||
if config.SASURL != "" {
|
||||
_, err := url.Parse(config.SASURL)
|
||||
return err
|
||||
|
@ -319,11 +339,8 @@ func ValidateAzBlobFsConfig(config *AzBlobFsConfig) error {
|
|||
if config.Container == "" {
|
||||
return errors.New("container cannot be empty")
|
||||
}
|
||||
if config.AccountName == "" || !config.AccountKey.IsValidInput() {
|
||||
return errors.New("credentials cannot be empty or invalid")
|
||||
}
|
||||
if config.AccountKey.IsEncrypted() && !config.AccountKey.IsValid() {
|
||||
return errors.New("invalid encrypted account_key")
|
||||
if err := checkAzCredentials(config); err != nil {
|
||||
return err
|
||||
}
|
||||
if config.KeyPrefix != "" {
|
||||
if strings.HasPrefix(config.KeyPrefix, "/") {
|
||||
|
|
|
@ -136,10 +136,10 @@ func (s *webDavServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
defer common.Connections.Remove(connection.GetID())
|
||||
|
||||
if !isCached {
|
||||
// we update last login and check for home directory only if the user is not cached
|
||||
// we check the home directory only if the user is not cached
|
||||
connection.Fs.CheckRootPath(connection.GetUsername(), user.GetUID(), user.GetGID())
|
||||
dataprovider.UpdateLastLogin(user) //nolint:errcheck
|
||||
}
|
||||
dataprovider.UpdateLastLogin(user) //nolint:errcheck
|
||||
|
||||
prefix := path.Join("/", user.Username)
|
||||
// see RFC4918, section 9.4
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/drakkan/sftpgo/dataprovider"
|
||||
"github.com/drakkan/sftpgo/httpclient"
|
||||
"github.com/drakkan/sftpgo/httpd"
|
||||
"github.com/drakkan/sftpgo/kms"
|
||||
"github.com/drakkan/sftpgo/logger"
|
||||
"github.com/drakkan/sftpgo/vfs"
|
||||
"github.com/drakkan/sftpgo/webdavd"
|
||||
|
@ -125,6 +126,12 @@ func TestMain(m *testing.M) {
|
|||
|
||||
httpConfig := config.GetHTTPConfig()
|
||||
httpConfig.Initialize(configDir)
|
||||
kmsConfig := config.GetKMSConfig()
|
||||
err = kmsConfig.Initialize()
|
||||
if err != nil {
|
||||
logger.ErrorToConsole("error initializing kms: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
httpdConf := config.GetHTTPDConfig()
|
||||
httpdConf.BindPort = 8078
|
||||
|
@ -861,10 +868,7 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
u := getTestUser()
|
||||
u.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
u.FsConfig.GCSConfig.Bucket = "test"
|
||||
u.FsConfig.GCSConfig.Credentials = vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: `{ "type": "service_account" }`,
|
||||
}
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret(`{ "type": "service_account" }`)
|
||||
|
||||
providerConf := config.GetProviderConf()
|
||||
providerConf.PreferDatabaseCredentials = true
|
||||
|
@ -885,10 +889,10 @@ func TestLoginWithDatabaseCredentials(t *testing.T) {
|
|||
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, vfs.SecretStatusAES256GCM, user.FsConfig.GCSConfig.Credentials.Status)
|
||||
assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.Payload)
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.AdditionalData)
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.Key)
|
||||
assert.Equal(t, kms.SecretStatusSecretBox, user.FsConfig.GCSConfig.Credentials.GetStatus())
|
||||
assert.NotEmpty(t, user.FsConfig.GCSConfig.Credentials.GetPayload())
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetAdditionalData())
|
||||
assert.Empty(t, user.FsConfig.GCSConfig.Credentials.GetKey())
|
||||
|
||||
assert.NoFileExists(t, credentialsFile)
|
||||
|
||||
|
@ -912,10 +916,7 @@ func TestLoginInvalidFs(t *testing.T) {
|
|||
u := getTestUser()
|
||||
u.FsConfig.Provider = dataprovider.GCSFilesystemProvider
|
||||
u.FsConfig.GCSConfig.Bucket = "test"
|
||||
u.FsConfig.GCSConfig.Credentials = vfs.Secret{
|
||||
Status: vfs.SecretStatusPlain,
|
||||
Payload: "invalid JSON for credentials",
|
||||
}
|
||||
u.FsConfig.GCSConfig.Credentials = kms.NewPlainSecret("invalid JSON for credentials")
|
||||
user, _, err := httpd.AddUser(u, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
Loading…
Reference in a new issue