From 557a8b683a472b95301c314207afad225f94060c Mon Sep 17 00:00:00 2001 From: jonaustin09 Date: Mon, 3 Jun 2024 14:01:45 -0400 Subject: [PATCH] feat: iam service hashicorp vault Use Vault as an IAM service. This is intended to be managed through the versitygw admin commands similar to the internal iam service. This uses the kv-v2 key/value secrets storage, and uses access key for the key and stores the JSON serialized account data as the value. This currently only supports roleid/rolesecret or root token authentication methods to Vault. --- auth/iam.go | 59 +++++++---- auth/iam_internal.go | 2 +- auth/iam_s3_object.go | 2 +- auth/iam_vault.go | 226 ++++++++++++++++++++++++++++++++++++++++++ cmd/versitygw/main.go | 168 +++++++++++++++++++++---------- go.mod | 8 ++ go.sum | 18 ++++ 7 files changed, 411 insertions(+), 72 deletions(-) create mode 100644 auth/iam_vault.go 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=