diff --git a/auth/iam.go b/auth/iam.go index 145c99e2..a5027c3e 100644 --- a/auth/iam.go +++ b/auth/iam.go @@ -48,28 +48,42 @@ type IAMService interface { Shutdown() error } -var ErrNoSuchUser = errors.New("user not found") +var ( + // ErrUserExists is returned when the user already exists + ErrUserExists = errors.New("user already exists") + // ErrNoSuchUser is returned when the user does not exist + ErrNoSuchUser = errors.New("user not found") +) type Opts struct { - Dir string - LDAPServerURL string - LDAPBindDN string - LDAPPassword string - LDAPQueryBase string - LDAPObjClasses string - LDAPAccessAtr string - LDAPSecretAtr string - LDAPRoleAtr string - S3Access string - S3Secret string - S3Region string - S3Bucket string - S3Endpoint string - S3DisableSSlVerfiy bool - S3Debug bool - CacheDisable bool - CacheTTL int - CachePrune int + Dir string + LDAPServerURL string + LDAPBindDN string + LDAPPassword string + LDAPQueryBase string + LDAPObjClasses string + LDAPAccessAtr string + LDAPSecretAtr string + LDAPRoleAtr string + VaultEndpointURL string + VaultSecretStoragePath string + VaultMountPath string + VaultRootToken string + VaultRoleId string + VaultRoleSecret string + VaultServerCert string + VaultClientCert string + VaultClientCertKey string + S3Access string + S3Secret string + S3Region string + S3Bucket string + S3Endpoint string + S3DisableSSlVerfiy bool + S3Debug bool + CacheDisable bool + CacheTTL int + CachePrune int } func New(o *Opts) (IAMService, error) { @@ -90,6 +104,11 @@ func New(o *Opts) (IAMService, error) { o.S3Endpoint, o.S3DisableSSlVerfiy, o.S3Debug) fmt.Printf("initializing S3 IAM with '%v/%v'\n", o.S3Endpoint, o.S3Bucket) + case o.VaultEndpointURL != "": + svc, err = NewVaultIAMService(o.VaultEndpointURL, o.VaultSecretStoragePath, + o.VaultMountPath, o.VaultRootToken, o.VaultRoleId, o.VaultRoleSecret, + o.VaultServerCert, o.VaultClientCert, o.VaultClientCertKey) + fmt.Printf("initializing Vault IAM with %q\n", o.VaultEndpointURL) default: // if no iam options selected, default to the single user mode fmt.Println("No IAM service configured, enabling single account mode") diff --git a/auth/iam_internal.go b/auth/iam_internal.go index c94a169b..d69a5f05 100644 --- a/auth/iam_internal.go +++ b/auth/iam_internal.go @@ -70,7 +70,7 @@ func (s *IAMServiceInternal) CreateAccount(account Account) error { _, ok := conf.AccessAccounts[account.Access] if ok { - return nil, fmt.Errorf("account already exists") + return nil, ErrUserExists } conf.AccessAccounts[account.Access] = account diff --git a/auth/iam_s3_object.go b/auth/iam_s3_object.go index b8b6a607..eec1fb49 100644 --- a/auth/iam_s3_object.go +++ b/auth/iam_s3_object.go @@ -104,7 +104,7 @@ func (s *IAMServiceS3) CreateAccount(account Account) error { _, ok := conf.AccessAccounts[account.Access] if ok { - return fmt.Errorf("account already exists") + return ErrUserExists } conf.AccessAccounts[account.Access] = account diff --git a/auth/iam_vault.go b/auth/iam_vault.go new file mode 100644 index 00000000..fe2d6be7 --- /dev/null +++ b/auth/iam_vault.go @@ -0,0 +1,226 @@ +// Copyright 2023 Versity Software +// This file is licensed under the Apache License, Version 2.0 +// (the "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package auth + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + vault "github.com/hashicorp/vault-client-go" + "github.com/hashicorp/vault-client-go/schema" +) + +type VaultIAMService struct { + client *vault.Client + reqOpts []vault.RequestOption + secretStoragePath string +} + +var _ IAMService = &VaultIAMService{} + +func NewVaultIAMService(endpoint, secretStoragePath, mountPath, rootToken, roleID, roleSecret, serverCert, clientCert, clientCertKey string) (IAMService, error) { + opts := []vault.ClientOption{ + vault.WithAddress(endpoint), + // set request timeout to 10 secs + vault.WithRequestTimeout(10 * time.Second), + } + if serverCert != "" { + tls := vault.TLSConfiguration{} + + tls.ServerCertificate.FromBytes = []byte(serverCert) + if clientCert != "" { + if clientCertKey == "" { + return nil, fmt.Errorf("client certificate and client certificate should both be specified") + } + + tls.ClientCertificate.FromBytes = []byte(clientCert) + tls.ClientCertificateKey.FromBytes = []byte(clientCertKey) + } + + opts = append(opts, vault.WithTLS(tls)) + } + + client, err := vault.New(opts...) + if err != nil { + return nil, fmt.Errorf("init vault client: %w", err) + } + + reqOpts := []vault.RequestOption{} + // if mount path is not specified, it defaults to "approle" + if mountPath != "" { + reqOpts = append(reqOpts, vault.WithMountPath(mountPath)) + } + + // Authentication + switch { + case rootToken != "": + err := client.SetToken(rootToken) + if err != nil { + return nil, fmt.Errorf("root token authentication failure: %w", err) + } + case roleID != "": + if roleSecret == "" { + return nil, fmt.Errorf("role id and role secret must both be specified") + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + resp, err := client.Auth.AppRoleLogin(ctx, schema.AppRoleLoginRequest{ + RoleId: roleID, + SecretId: roleSecret, + }, reqOpts...) + cancel() + if err != nil { + return nil, fmt.Errorf("approle authentication failure: %w", err) + } + + if err := client.SetToken(resp.Auth.ClientToken); err != nil { + return nil, fmt.Errorf("approle authentication set token failure: %w", err) + } + default: + return nil, fmt.Errorf("vault authentication requires either roleid/rolesecret or root token") + } + + return &VaultIAMService{ + client: client, + reqOpts: reqOpts, + secretStoragePath: secretStoragePath, + }, nil +} + +func (vt *VaultIAMService) CreateAccount(account Account) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + _, err := vt.client.Secrets.KvV2Write(ctx, vt.secretStoragePath+"/"+account.Access, schema.KvV2WriteRequest{ + Data: map[string]any{ + account.Access: account, + }, + Options: map[string]interface{}{ + "cas": 0, + }, + }, vt.reqOpts...) + cancel() + if err != nil { + if strings.Contains(err.Error(), "check-and-set") { + return ErrUserExists + } + return err + } + + return nil +} + +func (vt *VaultIAMService) GetUserAccount(access string) (Account, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + resp, err := vt.client.Secrets.KvV2Read(ctx, vt.secretStoragePath+"/"+access, vt.reqOpts...) + cancel() + if err != nil { + return Account{}, err + } + + acc, err := parseVaultUserAccount(resp.Data.Data, access) + if err != nil { + return Account{}, err + } + + return acc, nil +} + +func (vt *VaultIAMService) DeleteUserAccount(access string) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + _, err := vt.client.Secrets.KvV2DeleteMetadataAndAllVersions(ctx, vt.secretStoragePath+"/"+access, vt.reqOpts...) + cancel() + if err != nil { + return err + } + return nil +} + +func (vt *VaultIAMService) ListUserAccounts() ([]Account, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + resp, err := vt.client.Secrets.KvV2List(ctx, vt.secretStoragePath, vt.reqOpts...) + cancel() + if err != nil { + if vault.IsErrorStatus(err, 404) { + return []Account{}, nil + } + return nil, err + } + + accs := []Account{} + + for _, acss := range resp.Data.Keys { + acc, err := vt.GetUserAccount(acss) + if err != nil { + return nil, err + } + accs = append(accs, acc) + } + + return accs, nil +} + +// the client doesn't have explicit shutdown, as it uses http.Client +func (vt *VaultIAMService) Shutdown() error { + return nil +} + +var errInvalidUser error = errors.New("invalid user account entry in secrets engine") + +func parseVaultUserAccount(data map[string]interface{}, access string) (acc Account, err error) { + usrAcc, ok := data[access].(map[string]interface{}) + if !ok { + return acc, errInvalidUser + } + + acss, ok := usrAcc["access"].(string) + if !ok { + return acc, errInvalidUser + } + secret, ok := usrAcc["secret"].(string) + if !ok { + return acc, errInvalidUser + } + role, ok := usrAcc["role"].(string) + if !ok { + return acc, errInvalidUser + } + userIdJson, ok := usrAcc["userID"].(json.Number) + if !ok { + return acc, errInvalidUser + } + userId, err := userIdJson.Int64() + if err != nil { + return acc, errInvalidUser + } + groupIdJson, ok := usrAcc["groupID"].(json.Number) + if !ok { + return acc, errInvalidUser + } + groupId, err := groupIdJson.Int64() + if err != nil { + return acc, errInvalidUser + } + + return Account{ + Access: acss, + Secret: secret, + Role: Role(role), + UserID: int(userId), + GroupID: int(groupId), + }, nil +} diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go index 2b3fb802..c60cb34b 100644 --- a/cmd/versitygw/main.go +++ b/cmd/versitygw/main.go @@ -35,37 +35,42 @@ import ( ) var ( - port, admPort string - rootUserAccess string - rootUserSecret string - region string - admCertFile, admKeyFile string - certFile, keyFile string - kafkaURL, kafkaTopic, kafkaKey string - natsURL, natsTopic string - eventWebhookURL string - eventConfigFilePath string - logWebhookURL string - accessLog string - healthPath string - debug bool - pprof string - quiet bool - readonly bool - iamDir string - ldapURL, ldapBindDN, ldapPassword string - ldapQueryBase, ldapObjClasses string - ldapAccessAtr, ldapSecAtr, ldapRoleAtr string - s3IamAccess, s3IamSecret string - s3IamRegion, s3IamBucket string - s3IamEndpoint string - s3IamSslNoVerify, s3IamDebug bool - iamCacheDisable bool - iamCacheTTL int - iamCachePrune int - metricsService string - statsdServers string - dogstatsServers string + port, admPort string + rootUserAccess string + rootUserSecret string + region string + admCertFile, admKeyFile string + certFile, keyFile string + kafkaURL, kafkaTopic, kafkaKey string + natsURL, natsTopic string + eventWebhookURL string + eventConfigFilePath string + logWebhookURL string + accessLog string + healthPath string + debug bool + pprof string + quiet bool + readonly bool + iamDir string + ldapURL, ldapBindDN, ldapPassword string + ldapQueryBase, ldapObjClasses string + ldapAccessAtr, ldapSecAtr, ldapRoleAtr string + vaultEndpointURL, vaultSecretStoragePath string + vaultMountPath, vaultRootToken string + vaultRoleId, vaultRoleSecret string + vaultServerCert, vaultClientCert string + vaultClientCertKey string + s3IamAccess, s3IamSecret string + s3IamRegion, s3IamBucket string + s3IamEndpoint string + s3IamSslNoVerify, s3IamDebug bool + iamCacheDisable bool + iamCacheTTL int + iamCachePrune int + metricsService string + statsdServers string + dogstatsServers string ) var ( @@ -326,6 +331,60 @@ func initFlags() []cli.Flag { EnvVars: []string{"VGW_IAM_LDAP_ROLE_ATR"}, Destination: &ldapRoleAtr, }, + &cli.StringFlag{ + Name: "iam-vault-endpoint-url", + Usage: "vault server url", + EnvVars: []string{"VGW_IAM_VAULT_ENDPOINT_URL"}, + Destination: &vaultEndpointURL, + }, + &cli.StringFlag{ + Name: "iam-vault-secret-storage-path", + Usage: "vault server secret storage path", + EnvVars: []string{"VGW_IAM_VAULT_SECRET_STORAGE_PATH"}, + Destination: &vaultSecretStoragePath, + }, + &cli.StringFlag{ + Name: "iam-vault-mount-path", + Usage: "vault server mount path", + EnvVars: []string{"VGW_IAM_VAULT_MOUNT_PATH"}, + Destination: &vaultMountPath, + }, + &cli.StringFlag{ + Name: "iam-vault-root-token", + Usage: "vault server root token", + EnvVars: []string{"VGW_IAM_VAULT_ROOT_TOKEN"}, + Destination: &vaultRootToken, + }, + &cli.StringFlag{ + Name: "iam-vault-role-id", + Usage: "vault server user role id", + EnvVars: []string{"VGW_IAM_VAULT_ROLE_ID"}, + Destination: &vaultRoleId, + }, + &cli.StringFlag{ + Name: "iam-vault-role-secret", + Usage: "vault server user role secret", + EnvVars: []string{"VGW_IAM_VAULT_ROLE_SECRET"}, + Destination: &vaultRoleSecret, + }, + &cli.StringFlag{ + Name: "iam-vault-server_cert", + Usage: "vault server TLS certificate", + EnvVars: []string{"VGW_IAM_VAULT_SERVER_CERT"}, + Destination: &vaultServerCert, + }, + &cli.StringFlag{ + Name: "iam-vault-client_cert", + Usage: "vault client TLS certificate", + EnvVars: []string{"VGW_IAM_VAULT_CLIENT_CERT"}, + Destination: &vaultClientCert, + }, + &cli.StringFlag{ + Name: "iam-vault-client_cert_key", + Usage: "vault client TLS certificate key", + EnvVars: []string{"VGW_IAM_VAULT_CLIENT_CERT_KEY"}, + Destination: &vaultClientCertKey, + }, &cli.StringFlag{ Name: "s3-iam-access", Usage: "s3 IAM access key", @@ -501,25 +560,34 @@ func runGateway(ctx context.Context, be backend.Backend) error { } iam, err := auth.New(&auth.Opts{ - Dir: iamDir, - LDAPServerURL: ldapURL, - LDAPBindDN: ldapBindDN, - LDAPPassword: ldapPassword, - LDAPQueryBase: ldapQueryBase, - LDAPObjClasses: ldapObjClasses, - LDAPAccessAtr: ldapAccessAtr, - LDAPSecretAtr: ldapSecAtr, - LDAPRoleAtr: ldapRoleAtr, - S3Access: s3IamAccess, - S3Secret: s3IamSecret, - S3Region: s3IamRegion, - S3Bucket: s3IamBucket, - S3Endpoint: s3IamEndpoint, - S3DisableSSlVerfiy: s3IamSslNoVerify, - S3Debug: s3IamDebug, - CacheDisable: iamCacheDisable, - CacheTTL: iamCacheTTL, - CachePrune: iamCachePrune, + Dir: iamDir, + LDAPServerURL: ldapURL, + LDAPBindDN: ldapBindDN, + LDAPPassword: ldapPassword, + LDAPQueryBase: ldapQueryBase, + LDAPObjClasses: ldapObjClasses, + LDAPAccessAtr: ldapAccessAtr, + LDAPSecretAtr: ldapSecAtr, + LDAPRoleAtr: ldapRoleAtr, + VaultEndpointURL: vaultEndpointURL, + VaultSecretStoragePath: vaultSecretStoragePath, + VaultMountPath: vaultMountPath, + VaultRootToken: vaultRootToken, + VaultRoleId: vaultRoleId, + VaultRoleSecret: vaultRoleSecret, + VaultServerCert: vaultServerCert, + VaultClientCert: vaultClientCert, + VaultClientCertKey: vaultClientCertKey, + S3Access: s3IamAccess, + S3Secret: s3IamSecret, + S3Region: s3IamRegion, + S3Bucket: s3IamBucket, + S3Endpoint: s3IamEndpoint, + S3DisableSSlVerfiy: s3IamSslNoVerify, + S3Debug: s3IamDebug, + CacheDisable: iamCacheDisable, + CacheTTL: iamCacheTTL, + CachePrune: iamCachePrune, }) if err != nil { return fmt.Errorf("setup iam: %w", err) diff --git a/go.mod b/go.mod index eb2d67df..3d3c50c5 100644 --- a/go.mod +++ b/go.mod @@ -36,15 +36,23 @@ require ( github.com/aws/aws-sdk-go-v2/service/sts v1.28.11 // indirect github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-retryablehttp v0.7.1 // indirect + github.com/hashicorp/go-rootcerts v1.0.2 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/vault-client-go v0.4.3 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/nats-io/nkeys v0.4.7 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect golang.org/x/crypto v0.23.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/text v0.15.0 // indirect + golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect ) require ( diff --git a/go.sum b/go.sum index 575c463e..8a6c8b23 100644 --- a/go.sum +++ b/go.sum @@ -80,9 +80,21 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= +github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/vault-client-go v0.4.3 h1:zG7STGVgn/VK6rnZc0k8PGbfv2x/sJExRKHSUg3ljWc= +github.com/hashicorp/vault-client-go v0.4.3/go.mod h1:4tDw7Uhq5XOxS1fO+oMtotHL7j4sB9cp0T7U6m4FzDY= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -111,6 +123,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +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/nats-io/nats.go v1.35.0 h1:XFNqNM7v5B+MQMKqVGAyHwYhyKb48jrenXNxIU20ULk= github.com/nats-io/nats.go v1.35.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= @@ -132,6 +146,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 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/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0= github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= @@ -236,6 +252,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af h1:Yx9k8YCG3dvF87UAn2tu2HQLf2dt/eR1bXxpLMWeH+Y= +golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=