From 2452267329bc3e5f70e88bc88008d9b212ccaffb Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Tue, 2 Apr 2024 17:12:46 +0200 Subject: [PATCH 1/5] Support LDAP authentication with unauthorized reads --- BUILD.bazel | 1 + README.md | 52 ++++++++++++- WORKSPACE | 12 +++ config/config.go | 49 +++++++++++- config/config_test.go | 51 +++++++++++++ go.mod | 5 ++ go.sum | 22 ++++++ ldap/BUILD.bazel | 13 ++++ ldap/ldap.go | 169 ++++++++++++++++++++++++++++++++++++++++++ main.go | 24 ++++++ utils/flags/flags.go | 44 +++++++++++ 11 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 ldap/BUILD.bazel create mode 100644 ldap/ldap.go diff --git a/BUILD.bazel b/BUILD.bazel index c9b721e49..43fd8f123 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -26,6 +26,7 @@ go_library( deps = [ "//cache/disk:go_default_library", "//config:go_default_library", + "//ldap:go_default_library", "//server:go_default_library", "//utils/flags:go_default_library", "//utils/idle:go_default_library", diff --git a/README.md b/README.md index e4ed52a81..878e4d175 100644 --- a/README.md +++ b/README.md @@ -220,9 +220,10 @@ OPTIONS: [$BAZEL_REMOTE_TLS_KEY_FILE] --allow_unauthenticated_reads If authentication is enabled - (--htpasswd_file or --tls_ca_file), allow unauthenticated clients read - access. (default: false, ie if authentication is required, read-only - requests must also be authenticated) [$BAZEL_REMOTE_UNAUTHENTICATED_READS] + (--htpasswd_file, --tls_ca_file or --ldap.url), allow unauthenticated + clients read access. (default: false, i.e. if authentication is required, + read-only requests must also be authenticated) + [$BAZEL_REMOTE_UNAUTHENTICATED_READS] --idle_timeout value The maximum period of having received no request after which the server will shut itself down. (default: 0s, ie disabled) @@ -292,6 +293,34 @@ OPTIONS: Google credentials for the Google Cloud Storage proxy backend. [$BAZEL_REMOTE_GCS_JSON_CREDENTIALS_FILE] + --ldap.url value The LDAP URL which may include a port. LDAP over SSL + (LDAPs) is supported. + [$BAZEL_REMOTE_LDAP_URL] + + --ldap.base_dn value The distinguished name of the search base. + [$BAZEL_REMOTE_LDAP_BASE_DN] + + --ldap.bind_user value The user who is allowed to perform a search within + the base DN. If none is specified the connection and the search is + performed without an authentication. It is recommended to use a read-only + account. + [$BAZEL_REMOTE_LDAP_BIND_USER] + + --ldap.bind_password value The password of the bind user. + [$BAZEL_REMOTE_LDAP_BIND_PASSWORD] + + --ldap.username_attribute value The user attribute of a connecting user. + (default: "uid") + [$BAZEL_REMOTE_LDAP_USER_ATTRIBUTE] + + --ldap.groups value Filter clause for searching groups. This option can be + given multiple times and the groups are OR connected in the search query. + [$BAZEL_REMOTE_LDAP_GROUPS] + + --ldap.cache_time value The amount of time to cache a successful + authentication in seconds. (default 3600) + [$BAZEL_REMOTE_LDAP_CACHE_TIME] + --s3.endpoint value The S3/minio endpoint to use when using S3 proxy backend. [$BAZEL_REMOTE_S3_ENDPOINT] @@ -674,6 +703,7 @@ $ bazel build :bazel-remote ``` ### Authentication +#### htpasswd bazel-remote defaults to allow unauthenticated access, but basic `.htpasswd` style authentication and mutual TLS authentication are also supported. @@ -713,6 +743,22 @@ $ docker run -v /path/to/cache/dir:/data \ --max_size 5 ``` +#### LDAP + +Supported via a config file, env variables or command line args, see above. + +```yaml +ldap: + url: ldap://ldap.example.com # ldaps and custom port also supported + base_dn: OU=My Users,DC=example,DC=com # root of the tree to scope queries + username_attribute: sAMAccountName # defaults to "uid" + bind_user: ldapuser # (read-only) account for user lookup + bind_password: ldappassword + cache_time: 3600 # how long to cache a successful authentication for (default 1 hour) + groups: # if specified, user must be in one of these to access the cache + - CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com +``` + ### Using bazel-remote with AWS Credential file authentication for S3 inside a docker container The following demonstrates how to configure a docker instance of bazel-remote to use an AWS S3 diff --git a/WORKSPACE b/WORKSPACE index 83146392f..675229596 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -124,6 +124,18 @@ go_repository( version = "v1.3.5", ) +go_repository( + name = "in_gopkg_asn1_ber_v1", + commit = "f715ec2f112d1e4195b827ad68cf44017a3ef2b1", + importpath = "gopkg.in/asn1-ber.v1", +) + +go_repository( + name = "in_gopkg_ldap_v3", + commit = "9f0d712775a0973b7824a1585a86a4ea1d5263d9", + importpath = "gopkg.in/ldap.v3", +) + gazelle_dependencies() http_archive( diff --git a/config/config.go b/config/config.go index 51655d00d..1cf692374 100644 --- a/config/config.go +++ b/config/config.go @@ -38,6 +38,16 @@ type URLBackendConfig struct { CaFile string `yaml:"ca_file"` } +type LDAPConfig struct { + BaseURL string `yaml:"url"` + BaseDN string `yaml:"base_dn"` + BindUser string `yaml:"bind_user"` + BindPassword string `yaml:"bind_password"` + UsernameAttribute string `yaml:"username_attribute"` + Groups []string `yaml:"groups,flow"` + CacheTime time.Duration `yaml:"cache_time"` +} + func (c *URLBackendConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type Aux URLBackendConfig aux := &struct { @@ -99,6 +109,7 @@ type Config struct { GoogleCloudStorage *GoogleCloudStorageConfig `yaml:"gcs_proxy,omitempty"` HTTPBackend *URLBackendConfig `yaml:"http_proxy,omitempty"` GRPCBackend *URLBackendConfig `yaml:"grpc_proxy,omitempty"` + LDAP *LDAPConfig `yaml:"ldap,omitempty"` NumUploaders int `yaml:"num_uploaders"` MaxQueuedUploads int `yaml:"max_queued_uploads"` IdleTimeout time.Duration `yaml:"idle_timeout"` @@ -157,6 +168,7 @@ func newFromArgs(dir string, maxSize int, storageMode string, zstdImplementation hc *URLBackendConfig, grpcb *URLBackendConfig, gcs *GoogleCloudStorageConfig, + ldap *LDAPConfig, s3 *S3CloudStorageConfig, azblob *AzBlobStorageConfig, disableHTTPACValidation bool, @@ -192,6 +204,7 @@ func newFromArgs(dir string, maxSize int, storageMode string, zstdImplementation GoogleCloudStorage: gcs, HTTPBackend: hc, GRPCBackend: grpcb, + LDAP: ldap, IdleTimeout: idleTimeout, DisableHTTPACValidation: disableHTTPACValidation, DisableGRPCACDepsCheck: disableGRPCACDepsCheck, @@ -368,7 +381,7 @@ func validateConfig(c *Config) error { "and 'tls_cert_file' specified.") } - if c.AllowUnauthenticatedReads && c.TLSCaFile == "" && c.HtpasswdFile == "" { + if c.AllowUnauthenticatedReads && c.TLSCaFile == "" && c.HtpasswdFile == "" && c.LDAP.BaseURL == "" { return errors.New("AllowUnauthenticatedReads setting is only available when authentication is enabled") } @@ -450,6 +463,26 @@ func validateConfig(c *Config) error { } } + if c.HtpasswdFile != "" && c.LDAP != nil { + return errors.New("One can specify at most one authentication mechanism") + } + + if c.LDAP != nil { + // to allow anonymous access do not require BindUser or BindPassword + if c.LDAP.BaseURL == "" { + return errors.New("The 'url' field is required for 'ldap'") + } + if c.LDAP.BaseDN == "" { + return errors.New("The 'base_dn' field is required for 'ldap'") + } + if c.LDAP.UsernameAttribute == "" { + c.LDAP.UsernameAttribute = "uid" + } + if c.LDAP.CacheTime <= 0 { + c.LDAP.CacheTime = 3600 + } + } + switch c.AccessLogLevel { case "none", "all": default: @@ -590,6 +623,19 @@ func get(ctx *cli.Context) (*Config, error) { } } + var ldap *LDAPConfig + if ctx.String("ldap.url") != "" { + ldap = &LDAPConfig{ + BaseURL: ctx.String("ldap.url"), + BaseDN: ctx.String("ldap.base_dn"), + BindUser: ctx.String("ldap.bind_user"), + BindPassword: ctx.String("ldap.bind_password"), + UsernameAttribute: ctx.String("ldap.username_attribute"), + Groups: ctx.StringSlice("ldap.groups"), + CacheTime: ctx.Duration("ldap.cache_time"), + } + } + return newFromArgs( ctx.String("dir"), ctx.Int("max_size"), @@ -610,6 +656,7 @@ func get(ctx *cli.Context) (*Config, error) { hc, grpcb, gcs, + ldap, s3, azblob, ctx.Bool("disable_http_ac_validation"), diff --git a/config/config_test.go b/config/config_test.go index 429e87081..a100d766b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -231,6 +231,57 @@ s3_proxy: } } +func TestValidLDAPConfig(t *testing.T) { + yaml := `host: localhost +port: 8080 +dir: /opt/cache-dir +max_size: 100 +ldap: + url: ldap://ldap.example.com + base_dn: OU=My Users,DC=example,DC=com + username_attribute: sAMAccountName + bind_user: ldapuser + bind_password: ldappassword + cache_time: 3600s + groups: + - CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com + - CN=other-users,OU=Groups2,OU=Alien Users,DC=foo,DC=org +` + config, err := newFromYaml([]byte(yaml)) + if err != nil { + t.Fatal(err) + } + + expectedConfig := &Config{ + HTTPAddress: "localhost:8080", + Dir: "/opt/cache-dir", + MaxSize: 100, + StorageMode: "zstd", + ZstdImplementation: "go", + LDAP: &LDAPConfig{ + BaseURL: "ldap://ldap.example.com", + BaseDN: "OU=My Users,DC=example,DC=com", + BindUser: "ldapuser", + BindPassword: "ldappassword", + UsernameAttribute: "sAMAccountName", + Groups: []string{"CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com", "CN=other-users,OU=Groups2,OU=Alien Users,DC=foo,DC=org"}, + CacheTime: 3600 * time.Second, + }, + NumUploaders: 100, + MinTLSVersion: "1.0", + MaxQueuedUploads: 1000000, + MaxBlobSize: math.MaxInt64, + MaxProxyBlobSize: math.MaxInt64, + MetricsDurationBuckets: []float64{.5, 1, 2.5, 5, 10, 20, 40, 80, 160, 320}, + AccessLogLevel: "all", + LogTimezone: "UTC", + } + + if !cmp.Equal(config, expectedConfig) { + t.Fatalf("Expected '%+v' but got '%+v'", expectedConfig, config) + } +} + func TestValidProfiling(t *testing.T) { yaml := `host: localhost port: 1234 diff --git a/go.mod b/go.mod index 0bab02536..d9cb6c767 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,8 @@ require ( google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 google.golang.org/grpc v1.60.1 google.golang.org/protobuf v1.32.0 + gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d + gopkg.in/ldap.v3 v3.0.3 gopkg.in/yaml.v3 v3.0.1 ) @@ -41,12 +43,15 @@ require ( cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/longrunning v0.5.4 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 // indirect github.com/aws/aws-sdk-go v1.44.256 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect + github.com/go-ldap/ldap/v3 v3.4.6 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/snappy v0.0.4 // indirect diff --git a/go.sum b/go.sum index 49664dfa7..2269afc4e 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,13 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 h1:jp0dGvZ7ZK0mgqnTSClMxa5 github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1 h1:QSdcrd/UFJv6Bp/CfoVf2SrENpFn9P6Yh8yb+xNhYMM= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1/go.mod h1:eZ4g6GUvXiGulfIbbhh1Xr4XwUYaYaWMqzGD/284wCA= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= +github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 h1:BWe8a+f/t+7KY7zH2mqygeUD0t8hNFXe08p1Pb3/jKE= github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f h1:R2ZVGCZzU95oXFJxncosHS9LsX8N4/MYUdGGWOb2cFk= github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f/go.mod h1:l2P3JyHa+fjy5Bxol6y1u2o4DV/mv3QMBdBu2cNR53w= +github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/aws/aws-sdk-go v1.44.256 h1:O8VH+bJqgLDguqkH/xQBFz5o/YheeZqgcOYIgsTVWY4= github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -34,6 +37,10 @@ github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= +github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A= +github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= @@ -50,6 +57,7 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= @@ -120,11 +128,14 @@ github.com/slok/go-http-metrics v0.11.0 h1:ABJUpekCZSkQT1wQrFvS4kGbhea/w6ndFJaWJ github.com/slok/go-http-metrics v0.11.0/go.mod h1:ZGKeYG1ET6TEJpQx18BqAJAvxw9jBAZXCHU7bWQqqAc= github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/urfave/cli/v2 v2.27.0 h1:uNs1K8JwTFL84X68j5Fjny6hfANh9nTlJ6dRtZAFAHY= @@ -137,6 +148,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -148,6 +160,7 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= @@ -169,6 +182,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -176,6 +191,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -183,6 +200,7 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -210,11 +228,15 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= +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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ldap.v3 v3.0.3 h1:YKRHW/2sIl05JsCtx/5ZuUueFuJyoj/6+DGXe3wp6ro= +gopkg.in/ldap.v3 v3.0.3/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/ldap/BUILD.bazel b/ldap/BUILD.bazel new file mode 100644 index 000000000..8476c4b42 --- /dev/null +++ b/ldap/BUILD.bazel @@ -0,0 +1,13 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["ldap.go"], + importpath = "github.com/buchgr/bazel-remote/v2/ldap", + visibility = ["//visibility:public"], + deps = [ + "//config:go_default_library", + "@com_github_abbot_go_http_auth//:go_default_library", + "@in_gopkg_ldap_v3//:go_default_library", + ], +) diff --git a/ldap/ldap.go b/ldap/ldap.go new file mode 100644 index 000000000..378b2e9cb --- /dev/null +++ b/ldap/ldap.go @@ -0,0 +1,169 @@ +package ldap + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/buchgr/bazel-remote/v2/config" + + auth "github.com/abbot/go-http-auth" + ldap "gopkg.in/ldap.v3" +) + +// Cache represents a cache of LDAP query results so that many concurrent +// requests don't DDoS the LDAP server. +type Cache struct { + *auth.BasicAuth + m sync.Map + config *config.LDAPConfig +} + +type cacheEntry struct { + sync.Mutex + // Poor man's enum; nil pointer means uninitialized + authed *bool +} + +func New(config *config.LDAPConfig) (*Cache, error) { + conn, err := ldap.DialURL(config.BaseURL) + if err != nil { + return nil, err + } + defer conn.Close() + + // Test the configured bind credentials + if err = conn.Bind(config.BindUser, config.BindPassword); err != nil { + return nil, err + } + + return &Cache{ + config: config, + BasicAuth: &auth.BasicAuth{ + Realm: "Bazel remote cache", + }, + }, nil +} + +// Either query LDAP for a result or retrieve it from the cache +func (c *Cache) checkLdap(user, password string) bool { + k := [2]string{user, password} + v, _ := c.m.LoadOrStore(k, &cacheEntry{}) + ce := v.(*cacheEntry) + ce.Lock() + defer ce.Unlock() + if ce.authed != nil { + return *ce.authed + } + + // Not initialized; actually do the query and record the result + authed := c.query(user, password) + ce.authed = &authed + timeout := c.config.CacheTime * time.Second + // Don't cache a negative result for a long time; likely wrong password + if !authed { + timeout = 5 * time.Second + } + go func() { + <-time.After(timeout) + c.m.Delete(k) + }() + + return authed +} + +func (c *Cache) query(user, password string) bool { + // This should always succeed since it was tested at instantiation + conn, err := ldap.DialURL(c.config.BaseURL) + if err != nil { + panic(err) + } + defer conn.Close() + + if err = conn.Bind(c.config.BindUser, c.config.BindPassword); err != nil { + panic(err) + } + + var groupsQuery strings.Builder + if len(c.config.Groups) != 0 { + groupsQuery.WriteString("(|") + for _, group := range c.config.Groups { + // memberOf only works with groupOfNames, not with POSIX + fmt.Fprintf(&groupsQuery, "(memberOf=%s)", group) + } + groupsQuery.WriteString(")") + } + + query := fmt.Sprintf("(&(%s=%s)%s)", c.config.UsernameAttribute, user, groupsQuery.String()) + + searchRequest := ldap.NewSearchRequest( + c.config.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + query, + []string{"cn", "dn"}, + nil, + ) + + sr, err := conn.Search(searchRequest) + if err != nil || len(sr.Entries) != 1 { + return false + } + + // Do they have the right credentials? + return conn.Bind(sr.Entries[0].DN, password) == nil +} + +// Below mostly copied from github.com/abbot/go-http-auth +// in order to "override" CheckAuth + +func (c *Cache) CheckAuth(r *http.Request) string { + s := strings.SplitN(r.Header.Get(c.Headers.V().Authorization), " ", 2) + if len(s) != 2 || s[0] != "Basic" { + return "" + } + + b, err := base64.StdEncoding.DecodeString(s[1]) + if err != nil { + return "" + } + pair := strings.SplitN(string(b), ":", 2) + if len(pair) != 2 { + return "" + } + user, password := pair[0], pair[1] + if !c.checkLdap(user, password) { + return "" + } + + return user +} + +func (c *Cache) Wrap(wrapped auth.AuthenticatedHandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if username := c.CheckAuth(r); username == "" { + c.RequireAuth(w, r) + } else { + ar := &auth.AuthenticatedRequest{Request: *r, Username: username} + wrapped(w, ar) + } + } +} + +func (c *Cache) NewContext(ctx context.Context, r *http.Request) context.Context { + type key int + // key of context.WithValue must be comparable and should not be of type + // string or any other built-in type to avoid collisions between packages + // using context + var infoKey key + info := &auth.Info{Username: c.CheckAuth(r), ResponseHeaders: make(http.Header)} + + info.Authenticated = info.Username != "" + if !info.Authenticated { + info.ResponseHeaders.Set(c.Headers.V().Authenticate, `Basic realm="`+c.Realm+`"`) + } + return context.WithValue(ctx, infoKey, info) +} diff --git a/main.go b/main.go index 9472701b8..e0466d054 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,7 @@ import ( "github.com/buchgr/bazel-remote/v2/cache/disk" "github.com/buchgr/bazel-remote/v2/config" + "github.com/buchgr/bazel-remote/v2/ldap" "github.com/buchgr/bazel-remote/v2/server" "github.com/buchgr/bazel-remote/v2/utils/flags" "github.com/buchgr/bazel-remote/v2/utils/idle" @@ -48,6 +49,8 @@ func main() { cli.HelpPrinterCustom = flags.HelpPrinter // Force the use of cli.HelpPrinterCustom. app.ExtraInfo = func() map[string]string { return map[string]string{} } + // ldap groups could contain "," and would be splitted + app.SliceFlagSeparator = ";" app.Flags = flags.GetCliFlags() app.Action = run @@ -242,6 +245,7 @@ func startHttpServer(c *config.Config, httpServer **http.Server, c.EnableACKeyInstanceMangling, checkClientCertForReads, checkClientCertForWrites, gitCommit) cacheHandler := h.CacheHandler + var ldapAuthenticator auth.AuthenticatorInterface var basicAuthenticator auth.BasicAuth if c.HtpasswdFile != "" { if c.AllowUnauthenticatedReads { @@ -250,6 +254,18 @@ func startHttpServer(c *config.Config, httpServer **http.Server, basicAuthenticator = auth.BasicAuth{Realm: c.HTTPAddress, Secrets: htpasswdSecrets} cacheHandler = basicAuthWrapper(cacheHandler, &basicAuthenticator) } + } else if c.LDAP.BaseURL != "" { + if c.AllowUnauthenticatedReads { + cacheHandler = unauthenticatedReadWrapper(cacheHandler, htpasswdSecrets, c.HTTPAddress) + } else { + var ldap_err error + if ldapAuthenticator, ldap_err = ldap.New(c.LDAP); ldap_err != nil { + log.Fatal("Failed to create LDAP connection: ", ldap_err) + } + cacheHandler = ldapAuthWrapper(cacheHandler, ldapAuthenticator) + } + } else { + log.Println("Neither HTTP_PASSWD nor LDAP used") } if c.IdleTimeout > 0 { @@ -267,6 +283,8 @@ func startHttpServer(c *config.Config, httpServer **http.Server, statusHandler = h.VerifyClientCertHandler(statusHandler).ServeHTTP } else if c.HtpasswdFile != "" { statusHandler = basicAuthWrapper(statusHandler, &basicAuthenticator) + } else if c.LDAP.BaseURL != "" { + statusHandler = ldapAuthWrapper(statusHandler, ldapAuthenticator) } } @@ -285,6 +303,8 @@ func startHttpServer(c *config.Config, httpServer **http.Server, middlewareHandler = h.VerifyClientCertHandler(middlewareHandler) } else if c.HtpasswdFile != "" { middlewareHandler = basicAuthWrapper(middlewareHandler.ServeHTTP, &basicAuthenticator) + } else if c.LDAP.BaseURL != "" { + middlewareHandler = ldapAuthWrapper(middlewareHandler.ServeHTTP, ldapAuthenticator) } } mux.Handle("/metrics", middlewareHandler) @@ -438,6 +458,10 @@ func basicAuthWrapper(handler http.HandlerFunc, authenticator *auth.BasicAuth) h return auth.JustCheck(authenticator, handler) } +func ldapAuthWrapper(handler http.HandlerFunc, authenticator auth.AuthenticatorInterface) http.HandlerFunc { + return auth.JustCheck(authenticator, handler) +} + // A http.HandlerFunc wrapper which requires successful basic // authentication for write requests, but allows unauthenticated // read requests. diff --git a/utils/flags/flags.go b/utils/flags/flags.go index 10e3b0d40..2055be994 100644 --- a/utils/flags/flags.go +++ b/utils/flags/flags.go @@ -254,6 +254,50 @@ func GetCliFlags() []cli.Flag { Usage: "Path to a JSON file that contains Google credentials for the Google Cloud Storage proxy backend.", EnvVars: []string{"BAZEL_REMOTE_GCS_JSON_CREDENTIALS_FILE"}, }, + &cli.StringFlag{ + Name: "ldap.url", + Value: "", + Usage: "The LDAP URL which may include a port. LDAP over SSL (LDAPs) is supported.", + EnvVars: []string{"BAZEL_REMOTE_LDAP_URL"}, + }, + &cli.StringFlag{ + Name: "ldap.base_dn", + Value: "", + Usage: "The distinguished name of the search base.", + EnvVars: []string{"BAZEL_REMOTE_LDAP_BASE_DN"}, + }, + &cli.StringFlag{ + Name: "ldap.bind_user", + Value: "", + Usage: "The user who is allowed to perform a search within the base DN. If none is specified the connection and the search is performed without an authentication. It is recommended to use a read-only account.", + EnvVars: []string{"BAZEL_REMOTE_LDAP_BIND_USER"}, + }, + &cli.StringFlag{ + Name: "ldap.bind_password", + Value: "", + Usage: "The password of the bind user.", + EnvVars: []string{"BAZEL_REMOTE_LDAP_BIND_PASSWORD"}, + }, + &cli.StringFlag{ + Name: "ldap.username_attribute", + Value: "uid", + Usage: "The user attribute of a connecting user.", + EnvVars: []string{"BAZEL_REMOTE_LDAP_USER_ATTRIBUTE"}, + }, + // https://cli.urfave.org/v2/examples/flags/#multiple-values-per-single-flag + &cli.StringSliceFlag{ + Name: "ldap.groups", + // setting a "Value" will no longer respect the "SliceFlagSeparator" + // https://github.com/urfave/cli/issues/1878 + Usage: "Filter clause for searching groups. This option can be given multiple times and the groups are OR connected in the search query.", + EnvVars: []string{"BAZEL_REMOTE_LDAP_GROUPS"}, + }, + &cli.IntFlag{ + Name: "ldap.cache_time", + Value: 3600, + Usage: "The amount of time to cache a successful authentication in seconds.", + EnvVars: []string{"BAZEL_REMOTE_LDAP_CACHE_TIME"}, + }, &cli.StringFlag{ Name: "s3.endpoint", Value: "", From 6771babe392e9c5722a9a027e6f5d327dfd5ad5e Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Tue, 16 Apr 2024 09:05:53 +0200 Subject: [PATCH 2/5] Fixup review comments --- README.md | 43 ++++++++++++++++++++++++++++++------------- config/config.go | 13 ++++++------- config/config_test.go | 2 +- ldap/ldap.go | 9 +++++---- main.go | 8 ++++---- utils/flags/flags.go | 1 + 6 files changed, 47 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 878e4d175..8932f16b5 100644 --- a/README.md +++ b/README.md @@ -498,6 +498,17 @@ http_address: 0.0.0.0:8080 # Alternatively, you can use simple authentication: #htpasswd_file: path/to/.htpasswd +# At most one authentication mechanism can be used +#ldap: +# url: ldaps://ldap.example.com:636 +# base_dn: OU=My Users,DC=example,DC=com +# username_attribute: sAMAccountName # defaults to "uid" +# bind_user: ldapuser +# bind_password: ldappassword +# cache_time: 3600 # in seconds (default 1 hour) +# groups: +# - CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com + # If tls_ca_file or htpasswd_file are specified, you can choose @@ -703,10 +714,12 @@ $ bazel build :bazel-remote ``` ### Authentication -#### htpasswd bazel-remote defaults to allow unauthenticated access, but basic `.htpasswd` -style authentication and mutual TLS authentication are also supported. +style authentication, mutual TLS authentication and LDAP are also supported. +Please note that only one authentication mechanism can be used at a time. + +#### htpasswd In order to pass a `.htpasswd` and/or server key file(s) to the cache inside a docker container, you first need to mount the file in the @@ -725,6 +738,8 @@ $ docker run -v /path/to/cache/dir:/data \ --htpasswd_file /etc/bazel-remote/htpasswd --max_size 5 ``` +#### mTLS + If you prefer not using `.htpasswd` files it is also possible to authenticate with mTLS (also can be known as "authenticating client certificates"). You can do this by passing in the the cert/key the @@ -745,18 +760,20 @@ $ docker run -v /path/to/cache/dir:/data \ #### LDAP -Supported via a config file, env variables or command line args, see above. +LDAP is an additional authentication method for the cache. It can be used via +command line args, the config file or env variables. -```yaml -ldap: - url: ldap://ldap.example.com # ldaps and custom port also supported - base_dn: OU=My Users,DC=example,DC=com # root of the tree to scope queries - username_attribute: sAMAccountName # defaults to "uid" - bind_user: ldapuser # (read-only) account for user lookup - bind_password: ldappassword - cache_time: 3600 # how long to cache a successful authentication for (default 1 hour) - groups: # if specified, user must be in one of these to access the cache - - CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com +```bash +$ docker run -v /path/to/cache/dir:/data \ + -p 9090:8080 -p 9092:9092 buchgr/bazel-remote-cache \ + --ldap.url="ldaps://ldap.example.com:636" \ + --ldap.base_dn="OU=My Users,DC=example,DC=com" \ + --ldap.groups="CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com" \ + --ldap.groups="CN=bazel-testers,OU=Groups,OU=My Users,DC=example,DC=com" \ + --ldap.cache_time=100 \ + --ldap.bind_user="cn=readonly.username,ou=readonly,OU=Other Users,DC=example,DC=com" \ + --ldap.bind_password="secret4Sure" \ + --max_size 5 ``` ### Using bazel-remote with AWS Credential file authentication for S3 inside a docker container diff --git a/config/config.go b/config/config.go index 1cf692374..bfdcb6fd9 100644 --- a/config/config.go +++ b/config/config.go @@ -39,7 +39,7 @@ type URLBackendConfig struct { } type LDAPConfig struct { - BaseURL string `yaml:"url"` + URL string `yaml:"url"` BaseDN string `yaml:"base_dn"` BindUser string `yaml:"bind_user"` BindPassword string `yaml:"bind_password"` @@ -99,6 +99,7 @@ type Config struct { StorageMode string `yaml:"storage_mode"` ZstdImplementation string `yaml:"zstd_implementation"` HtpasswdFile string `yaml:"htpasswd_file"` + LDAP *LDAPConfig `yaml:"ldap,omitempty"` MinTLSVersion string `yaml:"min_tls_version"` TLSCaFile string `yaml:"tls_ca_file"` TLSCertFile string `yaml:"tls_cert_file"` @@ -109,7 +110,6 @@ type Config struct { GoogleCloudStorage *GoogleCloudStorageConfig `yaml:"gcs_proxy,omitempty"` HTTPBackend *URLBackendConfig `yaml:"http_proxy,omitempty"` GRPCBackend *URLBackendConfig `yaml:"grpc_proxy,omitempty"` - LDAP *LDAPConfig `yaml:"ldap,omitempty"` NumUploaders int `yaml:"num_uploaders"` MaxQueuedUploads int `yaml:"max_queued_uploads"` IdleTimeout time.Duration `yaml:"idle_timeout"` @@ -381,7 +381,7 @@ func validateConfig(c *Config) error { "and 'tls_cert_file' specified.") } - if c.AllowUnauthenticatedReads && c.TLSCaFile == "" && c.HtpasswdFile == "" && c.LDAP.BaseURL == "" { + if c.AllowUnauthenticatedReads && c.TLSCaFile == "" && c.HtpasswdFile == "" && c.LDAP.URL == "" { return errors.New("AllowUnauthenticatedReads setting is only available when authentication is enabled") } @@ -463,13 +463,12 @@ func validateConfig(c *Config) error { } } - if c.HtpasswdFile != "" && c.LDAP != nil { + if c.HtpasswdFile != "" && c.TLSCaFile != "" && c.LDAP != nil { return errors.New("One can specify at most one authentication mechanism") } if c.LDAP != nil { - // to allow anonymous access do not require BindUser or BindPassword - if c.LDAP.BaseURL == "" { + if c.LDAP.URL == "" { return errors.New("The 'url' field is required for 'ldap'") } if c.LDAP.BaseDN == "" { @@ -626,7 +625,7 @@ func get(ctx *cli.Context) (*Config, error) { var ldap *LDAPConfig if ctx.String("ldap.url") != "" { ldap = &LDAPConfig{ - BaseURL: ctx.String("ldap.url"), + URL: ctx.String("ldap.url"), BaseDN: ctx.String("ldap.base_dn"), BindUser: ctx.String("ldap.bind_user"), BindPassword: ctx.String("ldap.bind_password"), diff --git a/config/config_test.go b/config/config_test.go index a100d766b..c0425470d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -259,7 +259,7 @@ ldap: StorageMode: "zstd", ZstdImplementation: "go", LDAP: &LDAPConfig{ - BaseURL: "ldap://ldap.example.com", + URL: "ldap://ldap.example.com", BaseDN: "OU=My Users,DC=example,DC=com", BindUser: "ldapuser", BindPassword: "ldappassword", diff --git a/ldap/ldap.go b/ldap/ldap.go index 378b2e9cb..1e4335d09 100644 --- a/ldap/ldap.go +++ b/ldap/ldap.go @@ -4,6 +4,7 @@ import ( "context" "encoding/base64" "fmt" + "log" "net/http" "strings" "sync" @@ -30,7 +31,7 @@ type cacheEntry struct { } func New(config *config.LDAPConfig) (*Cache, error) { - conn, err := ldap.DialURL(config.BaseURL) + conn, err := ldap.DialURL(config.URL) if err != nil { return nil, err } @@ -78,14 +79,14 @@ func (c *Cache) checkLdap(user, password string) bool { func (c *Cache) query(user, password string) bool { // This should always succeed since it was tested at instantiation - conn, err := ldap.DialURL(c.config.BaseURL) + conn, err := ldap.DialURL(c.config.URL) if err != nil { - panic(err) + log.Fatal("No valid LDAP connection could be established:", err) } defer conn.Close() if err = conn.Bind(c.config.BindUser, c.config.BindPassword); err != nil { - panic(err) + log.Fatal("LDAP connection with username and password failed:", err) } var groupsQuery strings.Builder diff --git a/main.go b/main.go index e0466d054..2158058cd 100644 --- a/main.go +++ b/main.go @@ -49,7 +49,7 @@ func main() { cli.HelpPrinterCustom = flags.HelpPrinter // Force the use of cli.HelpPrinterCustom. app.ExtraInfo = func() map[string]string { return map[string]string{} } - // ldap groups could contain "," and would be splitted + // ldap groups could contain "," and would be split app.SliceFlagSeparator = ";" app.Flags = flags.GetCliFlags() @@ -254,7 +254,7 @@ func startHttpServer(c *config.Config, httpServer **http.Server, basicAuthenticator = auth.BasicAuth{Realm: c.HTTPAddress, Secrets: htpasswdSecrets} cacheHandler = basicAuthWrapper(cacheHandler, &basicAuthenticator) } - } else if c.LDAP.BaseURL != "" { + } else if c.LDAP != nil { if c.AllowUnauthenticatedReads { cacheHandler = unauthenticatedReadWrapper(cacheHandler, htpasswdSecrets, c.HTTPAddress) } else { @@ -283,7 +283,7 @@ func startHttpServer(c *config.Config, httpServer **http.Server, statusHandler = h.VerifyClientCertHandler(statusHandler).ServeHTTP } else if c.HtpasswdFile != "" { statusHandler = basicAuthWrapper(statusHandler, &basicAuthenticator) - } else if c.LDAP.BaseURL != "" { + } else if c.LDAP.URL != "" { statusHandler = ldapAuthWrapper(statusHandler, ldapAuthenticator) } } @@ -303,7 +303,7 @@ func startHttpServer(c *config.Config, httpServer **http.Server, middlewareHandler = h.VerifyClientCertHandler(middlewareHandler) } else if c.HtpasswdFile != "" { middlewareHandler = basicAuthWrapper(middlewareHandler.ServeHTTP, &basicAuthenticator) - } else if c.LDAP.BaseURL != "" { + } else if c.LDAP.URL != "" { middlewareHandler = ldapAuthWrapper(middlewareHandler.ServeHTTP, ldapAuthenticator) } } diff --git a/utils/flags/flags.go b/utils/flags/flags.go index 2055be994..2beef5578 100644 --- a/utils/flags/flags.go +++ b/utils/flags/flags.go @@ -266,6 +266,7 @@ func GetCliFlags() []cli.Flag { Usage: "The distinguished name of the search base.", EnvVars: []string{"BAZEL_REMOTE_LDAP_BASE_DN"}, }, + // to allow anonymous access do not require BindUser or BindPassword &cli.StringFlag{ Name: "ldap.bind_user", Value: "", From e2a1d7442c26dd953be9b57432fb6263f78c4537 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Thu, 18 Apr 2024 09:52:30 +0200 Subject: [PATCH 3/5] Add end-to-end test for LDAP * use LDAP fake server package * expose config from YAML function * update go repository and deps list --- WORKSPACE | 9 +- config/config.go | 4 + deps.bzl | 90 +++++++++++++ go.mod | 63 +++++----- go.sum | 177 +++++++++++++------------- ldap/BUILD.bazel | 14 ++- ldap/ldap.go | 2 +- ldap/ldap_test.go | 312 ++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 542 insertions(+), 129 deletions(-) create mode 100644 ldap/ldap_test.go diff --git a/WORKSPACE b/WORKSPACE index 675229596..41ee25b56 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -126,14 +126,9 @@ go_repository( go_repository( name = "in_gopkg_asn1_ber_v1", - commit = "f715ec2f112d1e4195b827ad68cf44017a3ef2b1", importpath = "gopkg.in/asn1-ber.v1", -) - -go_repository( - name = "in_gopkg_ldap_v3", - commit = "9f0d712775a0973b7824a1585a86a4ea1d5263d9", - importpath = "gopkg.in/ldap.v3", + sum = "h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=", + version = "v1.0.0-20181015200546-f715ec2f112d", ) gazelle_dependencies() diff --git a/config/config.go b/config/config.go index bfdcb6fd9..6cfc98604 100644 --- a/config/config.go +++ b/config/config.go @@ -291,6 +291,10 @@ func newFromYaml(data []byte) (*Config, error) { return &c, nil } +func NewConfigFromYaml(data []byte) (*Config, error) { + return newFromYaml(data) +} + func validateConfig(c *Config) error { if c.Dir == "" { return errors.New("The 'dir' flag/key is required") diff --git a/deps.bzl b/deps.bzl index b811a3d66..8d760db37 100644 --- a/deps.bzl +++ b/deps.bzl @@ -28,6 +28,13 @@ def go_dependencies(): sum = "h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=", version = "v0.0.0-20211218093645-b94a6e3cc137", ) + go_repository( + name = "com_github_alexbrainman_sspi", + importpath = "github.com/alexbrainman/sspi", + sum = "h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=", + version = "v0.0.0-20231016080023-1a75b4708caa", + ) + go_repository( name = "com_github_andybalholm_brotli", importpath = "github.com/andybalholm/brotli", @@ -65,6 +72,13 @@ def go_dependencies(): sum = "h1:QSdcrd/UFJv6Bp/CfoVf2SrENpFn9P6Yh8yb+xNhYMM=", version = "v0.4.1", ) + go_repository( + name = "com_github_azure_go_ntlmssp", + importpath = "github.com/Azure/go-ntlmssp", + sum = "h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=", + version = "v0.0.0-20221128193559-754e69321358", + ) + go_repository( name = "com_github_azuread_microsoft_authentication_library_for_go", importpath = "github.com/AzureAD/microsoft-authentication-library-for-go", @@ -230,6 +244,13 @@ def go_dependencies(): sum = "h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=", version = "v1.9.1", ) + go_repository( + name = "com_github_go_asn1_ber_asn1_ber", + importpath = "github.com/go-asn1-ber/asn1-ber", + sum = "h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=", + version = "v1.5.5", + ) + go_repository( name = "com_github_go_chi_chi_v4", importpath = "github.com/go-chi/chi/v4", @@ -243,6 +264,12 @@ def go_dependencies(): sum = "h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=", version = "v0.2.1", ) + go_repository( + name = "com_github_go_ldap_ldap_v3", + importpath = "github.com/go-ldap/ldap/v3", + sum = "h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=", + version = "v3.4.8", + ) go_repository( name = "com_github_go_logfmt_logfmt", @@ -371,6 +398,18 @@ def go_dependencies(): sum = "h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=", version = "v1.8.0", ) + go_repository( + name = "com_github_gorilla_securecookie", + importpath = "github.com/gorilla/securecookie", + sum = "h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=", + version = "v1.1.1", + ) + go_repository( + name = "com_github_gorilla_sessions", + importpath = "github.com/gorilla/sessions", + sum = "h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=", + version = "v1.2.1", + ) go_repository( name = "com_github_grpc_ecosystem_go_grpc_prometheus", @@ -378,18 +417,69 @@ def go_dependencies(): sum = "h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=", version = "v1.2.0", ) + go_repository( + name = "com_github_hashicorp_go_uuid", + importpath = "github.com/hashicorp/go-uuid", + sum = "h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=", + version = "v1.0.3", + ) + go_repository( name = "com_github_iris_contrib_schema", importpath = "github.com/iris-contrib/schema", sum = "h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=", version = "v0.0.6", ) + go_repository( + name = "com_github_jcmturner_aescts_v2", + importpath = "github.com/jcmturner/aescts/v2", + sum = "h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=", + version = "v2.0.0", + ) + go_repository( + name = "com_github_jcmturner_dnsutils_v2", + importpath = "github.com/jcmturner/dnsutils/v2", + sum = "h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=", + version = "v2.0.0", + ) + go_repository( + name = "com_github_jcmturner_gofork", + importpath = "github.com/jcmturner/gofork", + sum = "h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=", + version = "v1.7.6", + ) + go_repository( + name = "com_github_jcmturner_goidentity_v6", + importpath = "github.com/jcmturner/goidentity/v6", + sum = "h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=", + version = "v6.0.1", + ) + go_repository( + name = "com_github_jcmturner_gokrb5_v8", + importpath = "github.com/jcmturner/gokrb5/v8", + sum = "h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=", + version = "v8.4.4", + ) + go_repository( + name = "com_github_jcmturner_rpc_v2", + importpath = "github.com/jcmturner/rpc/v2", + sum = "h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=", + version = "v2.0.3", + ) + go_repository( name = "com_github_joker_jade", importpath = "github.com/Joker/jade", sum = "h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=", version = "v1.1.3", ) + go_repository( + name = "com_github_jonasscharpf_godap", + importpath = "github.com/JonasScharpf/godap", + sum = "h1:7L5zT1awL4RZeLtT4vp+BlRoTrFBbRtMFOZMQCqub7I=", + version = "v0.0.0-20240417153024-2d460c2776c0", + ) + go_repository( name = "com_github_josharian_intern", importpath = "github.com/josharian/intern", diff --git a/go.mod b/go.mod index d9cb6c767..bbaca1e5c 100644 --- a/go.mod +++ b/go.mod @@ -2,28 +2,26 @@ module github.com/buchgr/bazel-remote/v2 require ( github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/djherbis/atime v1.1.0 - github.com/golang/protobuf v1.5.3 + github.com/golang/protobuf v1.5.4 github.com/google/go-cmp v0.6.0 - github.com/google/uuid v1.5.0 + github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 - github.com/klauspost/compress v1.17.4 - github.com/minio/minio-go/v7 v7.0.66 + github.com/klauspost/compress v1.17.8 + github.com/minio/minio-go/v7 v7.0.69 github.com/mostynb/go-grpc-compression v1.2.2 github.com/mostynb/zstdpool-syncpool v0.0.13 - github.com/prometheus/client_golang v1.17.0 - github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/client_golang v1.19.0 + github.com/prometheus/client_model v0.6.1 // indirect github.com/slok/go-http-metrics v0.11.0 - github.com/urfave/cli/v2 v2.27.0 - golang.org/x/oauth2 v0.15.0 - golang.org/x/sync v0.5.0 - golang.org/x/sys v0.15.0 // indirect - google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 - google.golang.org/grpc v1.60.1 - google.golang.org/protobuf v1.32.0 - gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d - gopkg.in/ldap.v3 v3.0.3 + github.com/urfave/cli/v2 v2.27.1 + golang.org/x/oauth2 v0.19.0 + golang.org/x/sync v0.7.0 + golang.org/x/sys v0.19.0 // indirect + google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda + google.golang.org/grpc v1.63.2 + google.golang.org/protobuf v1.33.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -31,54 +29,53 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1 + github.com/JonasScharpf/godap v0.0.0-20240417153024-2d460c2776c0 + github.com/go-ldap/ldap/v3 v3.4.8 github.com/johannesboyne/gofakes3 v0.0.0-20230506070712-04da935ef877 github.com/valyala/gozstd v1.20.1 - google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 - google.golang.org/genproto/googleapis/bytestream v0.0.0-20231212172506-995d672761c0 - google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 + google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda + google.golang.org/genproto/googleapis/bytestream v0.0.0-20240401170217-c3f982113cda + google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda ) require ( - cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go/compute v1.25.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/longrunning v0.5.4 // indirect + cloud.google.com/go/longrunning v0.5.6 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 // indirect github.com/aws/aws-sdk-go v1.44.256 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect - github.com/go-ldap/ldap/v3 v3.4.6 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/sha256-simd v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect - github.com/prometheus/common v0.45.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/prometheus/common v0.52.3 // indirect + github.com/prometheus/procfs v0.13.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rs/xid v1.5.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 // indirect github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.19.0 // indirect + github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/net v0.24.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.8.0 // indirect - google.golang.org/appengine v1.6.8 // indirect + gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index 2269afc4e..c35152a88 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU= +cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/longrunning v0.5.4 h1:w8xEcbZodnA2BbW6sVirkkoC+1gP8wS57EUUgGS0GVg= -cloud.google.com/go/longrunning v0.5.4/go.mod h1:zqNVncI0BOP8ST6XQD1+VcvuShMmq7+xFSzOL++V0dI= +cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE= +cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0 h1:sVPhtT2qjO86rTUaWMr4WoES4TkjGnzcioXcnHV9s5k= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.0.0/go.mod h1:uGG2W01BaETf0Ozp+QxxKJdMBNRWPdstHG0Fmdwn1/U= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8= @@ -16,17 +16,20 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 h1:BWe8a+f/t+7KY7zH2mqygeUD0t8hNFXe08p1Pb3/jKE= github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= +github.com/JonasScharpf/godap v0.0.0-20240417153024-2d460c2776c0 h1:7L5zT1awL4RZeLtT4vp+BlRoTrFBbRtMFOZMQCqub7I= +github.com/JonasScharpf/godap v0.0.0-20240417153024-2d460c2776c0/go.mod h1:K5gGJQ/vwxHEUWBYv7ciUOgw94MYrGCoy5JX/hjSJkY= github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f h1:R2ZVGCZzU95oXFJxncosHS9LsX8N4/MYUdGGWOb2cFk= github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f/go.mod h1:l2P3JyHa+fjy5Bxol6y1u2o4DV/mv3QMBdBu2cNR53w= -github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= +github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/aws/aws-sdk-go v1.44.256 h1:O8VH+bJqgLDguqkH/xQBFz5o/YheeZqgcOYIgsTVWY4= github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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= @@ -34,34 +37,46 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/djherbis/atime v1.1.0 h1:rgwVbP/5by8BvvjBNrbh64Qz33idKT3pSnMSJsxhi0g= github.com/djherbis/atime v1.1.0/go.mod h1:28OF6Y8s3NQWwacXc5eZTsEsiMzp7LF8MbXE+XJPdBE= github.com/dnaeon/go-vcr v1.1.0 h1:ReYa/UBrRyQdant9B4fNHGoCNKw6qh6P0fsdGmZpR7c= -github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-ldap/ldap/v3 v3.4.6 h1:ert95MdbiG7aWo/oPYp9btL3KJlMPKnP58r09rI8T+A= -github.com/go-ldap/ldap/v3 v3.4.6/go.mod h1:IGMQANNtxpsOzj7uUAMjpGBaOVTC4DYyIy8VsTdxmtc= +github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= +github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +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/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= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 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/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= @@ -69,23 +84,20 @@ github.com/johannesboyne/gofakes3 v0.0.0-20230506070712-04da935ef877 h1:O7syWuYG github.com/johannesboyne/gofakes3 v0.0.0-20230506070712-04da935ef877/go.mod h1:AxgWC4DDX54O2WDoQO1Ceabtn6IbktjU/7bigor+66g= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= -github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.66 h1:bnTOXOHjOqv/gcMuiVbN9o2ngRItvqE774dG9nq0Dzw= -github.com/minio/minio-go/v7 v7.0.66/go.mod h1:DHAgmyQEGdW3Cif0UooKOyrT3Vxs82zNdV6tkKhRtbs= +github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0= +github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -99,19 +111,19 @@ github.com/mostynb/go-grpc-compression v1.2.2/go.mod h1:GOCr2KBxXcblCuczg3YdLQlc github.com/mostynb/zstdpool-syncpool v0.0.13 h1:AIzAvQ9hNum4Fh5jYXyfZTd2aDi1leq7grKDkVZX4+s= github.com/mostynb/zstdpool-syncpool v0.0.13/go.mod h1:pbt8qOdq6wX5jrUsRI9UmBvAnjToEgVQC3H1pwJwktM= github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= -github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= -github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= -github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA= +github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U= +github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= +github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= @@ -122,85 +134,87 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500 h1:WnNuhiq+FOY3jNj6JXFT+eLN3CQ/oPIsDPRanvwsmbI= github.com/shabbyrobe/gocovmerge v0.0.0-20190829150210-3e036491d500/go.mod h1:+njLrG5wSeoG4Ds61rFgEzKvenR2UHbjMoDHsczxly0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/slok/go-http-metrics v0.11.0 h1:ABJUpekCZSkQT1wQrFvS4kGbhea/w6ndFJaWJeh3zL0= github.com/slok/go-http-metrics v0.11.0/go.mod h1:ZGKeYG1ET6TEJpQx18BqAJAvxw9jBAZXCHU7bWQqqAc= github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/urfave/cli/v2 v2.27.0 h1:uNs1K8JwTFL84X68j5Fjny6hfANh9nTlJ6dRtZAFAHY= -github.com/urfave/cli/v2 v2.27.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= +github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g= github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI= -github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw= +github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/oauth2 v0.15.0 h1:s8pnnxNVzjWyrvYdFUQq5llS1PX2zhPXmccZv99h7uQ= -golang.org/x/oauth2 v0.15.0/go.mod h1:q48ptWNTY5XWf+JNten23lcvHpLJ0ZSxF5ttTHKVCAM= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -211,37 +225,28 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= -google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0 h1:s1w3X6gQxwrLEpxnLd/qXTVLgQE2yXwaOaoa6IlY/+o= -google.golang.org/genproto/googleapis/api v0.0.0-20231212172506-995d672761c0/go.mod h1:CAny0tYF+0/9rmDB9fahA9YLzX3+AEVl1qXbv5hhj6c= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20231212172506-995d672761c0 h1:Y6QQt9D/syZt/Qgnz5a1y2O3WunQeeVDfS9+Xr82iFA= -google.golang.org/genproto/googleapis/bytestream v0.0.0-20231212172506-995d672761c0/go.mod h1:guYXGPwC6jwxgWKW5Y405fKWOFNwlvUlUnzyp9i0uqo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda h1:wu/KJm9KJwpfHWhkkZGohVC6KRrc1oJNr4jwtQMOQXw= +google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda/go.mod h1:g2LLCvCeCSir/JJSWosk19BR4NVxGqHUC6rxIRsd7Aw= +google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda h1:b6F6WIV4xHHD0FA4oIyzU6mHWg2WI2X1RBehwa5QN38= +google.golang.org/genproto/googleapis/api v0.0.0-20240401170217-c3f982113cda/go.mod h1:AHcE/gZH76Bk/ROZhQphlRoWo5xKDEtz3eVEO1LfA8c= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240401170217-c3f982113cda h1:O77/tf8XXfErAKafUOaAtuDyynGoufcD0mnG++LziIs= +google.golang.org/genproto/googleapis/bytestream v0.0.0-20240401170217-c3f982113cda/go.mod h1:IN9OQUXZ0xT+26MDwZL8fJcYw+y99b0eYPA2U15Jt8o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ldap.v3 v3.0.3 h1:YKRHW/2sIl05JsCtx/5ZuUueFuJyoj/6+DGXe3wp6ro= -gopkg.in/ldap.v3 v3.0.3/go.mod h1:oxD7NyBuxchC+SgJDE1Q5Od05eGt29SDQVBmV+HYbzw= gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ldap/BUILD.bazel b/ldap/BUILD.bazel index 8476c4b42..0793e60c8 100644 --- a/ldap/BUILD.bazel +++ b/ldap/BUILD.bazel @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", @@ -8,6 +8,16 @@ go_library( deps = [ "//config:go_default_library", "@com_github_abbot_go_http_auth//:go_default_library", - "@in_gopkg_ldap_v3//:go_default_library", + "@com_github_go_ldap_ldap_v3//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["ldap_test.go"], + embed = [":go_default_library"], + deps = [ + "//config:go_default_library", + "@com_github_jonasscharpf_godap//godap:go_default_library", ], ) diff --git a/ldap/ldap.go b/ldap/ldap.go index 1e4335d09..51851e66b 100644 --- a/ldap/ldap.go +++ b/ldap/ldap.go @@ -13,7 +13,7 @@ import ( "github.com/buchgr/bazel-remote/v2/config" auth "github.com/abbot/go-http-auth" - ldap "gopkg.in/ldap.v3" + ldap "github.com/go-ldap/ldap/v3" ) // Cache represents a cache of LDAP query results so that many concurrent diff --git a/ldap/ldap_test.go b/ldap/ldap_test.go new file mode 100644 index 000000000..958223c49 --- /dev/null +++ b/ldap/ldap_test.go @@ -0,0 +1,312 @@ +package ldap + +import ( + "context" + b64 "encoding/base64" + "fmt" + "io" + "log" + "net/http" + "strings" + "sync" + "testing" + "time" + + "github.com/JonasScharpf/godap/godap" + simplesearch "github.com/JonasScharpf/godap/godap" + "github.com/abbot/go-http-auth" + config "github.com/buchgr/bazel-remote/v2/config" +) + +func loadYamlConfig(data []byte) *config.Config { + cfg, err := config.NewConfigFromYaml(data) + if err != nil { + log.Fatal(err) + } + return cfg +} + +func loadFakeLdapConfig() *config.Config { + yaml := `host: localhost +port: 8080 +dir: /opt/cache-dir +max_size: 100 +ldap: + url: ldap://127.0.0.99:10000/ + base_dn: OU=My Users,DC=example,DC=com + username_attribute: uid + bind_user: CN=read-only-admin,OU=My Users,DC=example,DC=com + bind_password: 1234 + cache_time: 3600s + groups: + - CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com + - CN=other-users,OU=Groups2,OU=Alien Users,DC=foo,DC=org +` + + return loadYamlConfig([]byte(yaml)) +} + +var usersPasswords = map[string]string{ + "CN=read-only-admin,OU=My Users,DC=example,DC=com": "1234", + "user": "password", + "cn=user,OU=My Users,DC=example,DC=com": "password", +} + +func verifyUserPass(username string, password string) bool { + log.Printf("Looking for username '%s' with password '%s'", username, password) + wantPass, hasUser := usersPasswords[username] + if !hasUser { + log.Printf("No such user '%s'", username) + return false + } + if wantPass == password { + log.Println("Password and username are valid") + return true + } + log.Printf("Invalid password for username '%s'", username) + return false +} + +func startLdapServer() { + hs := make([]godap.LDAPRequestHandler, 0) + + // use a LDAPBindFuncHandler to provide a callback function to respond + // to bind requests + hs = append(hs, &godap.LDAPBindFuncHandler{ + LDAPBindFunc: func(binddn string, bindpw []byte) bool { + return verifyUserPass(binddn, string(bindpw)) + }, + }) + + // use a LDAPSimpleSearchFuncHandler to reply to search queries + hs = append(hs, &simplesearch.LDAPSimpleSearchFuncHandler{ + LDAPSimpleSearchFunc: func(req *godap.LDAPSimpleSearchRequest) []*godap.LDAPSimpleSearchResultEntry { + ret := make([]*godap.LDAPSimpleSearchResultEntry, 0, 1) + + if req.FilterAttr == "uid" { + userPassword := b64.StdEncoding.EncodeToString([]byte(req.FilterValue)) + + ret = append(ret, &simplesearch.LDAPSimpleSearchResultEntry{ + DN: "cn=" + req.FilterValue + "," + req.BaseDN, + Attrs: map[string]interface{}{ + "cn": req.FilterValue, + "sn": req.FilterValue, + "uid": req.FilterValue, + "userPassword": userPassword, + "homeDirectory": "/home/" + req.FilterValue, + "objectClass": []string{ + "top", + "posixAccount", + "inetOrgPerson", + }, + }, + Skip: false, + }) + } else if req.FilterAttr == "searchFingerprint" { + // a non-simple search request has been received and should be + // processed. For simplicity, as this is just a fake LDAP + // server simple but really bad assumptions are done onwards. + // If the first query element is "pass" a response is sent, + // otherwise not. By this a user can be available/found or not + filterValues := strings.Split(req.FilterValue, ";") + passOrFail := filterValues[0] + user := filterValues[1] + userPassword := b64.StdEncoding.EncodeToString([]byte(user)) + // TODO add user with this password to the mapping + + if passOrFail == "pass" { + log.Println("Simulate 'query match'") + ret = append(ret, &simplesearch.LDAPSimpleSearchResultEntry{ + DN: "cn=" + user + "," + req.BaseDN, + Attrs: map[string]interface{}{ + "cn": user, + "sn": user, + "uid": user, + "userPassword": userPassword, + "homeDirectory": "/home/" + user, + "objectClass": []string{ + "top", + "posixAccount", + "inetOrgPerson", + }, + }, + Skip: false, + }) + } else { + log.Println("Simulate 'no query match'") + // "skip" this one in the LDAP processing step to mock an + // empty response + ret = append(ret, &simplesearch.LDAPSimpleSearchResultEntry{ + DN: "cn=" + user + "," + req.BaseDN, + Attrs: map[string]interface{}{ + "cn": user, + }, + Skip: true, + }) + } + } + + return ret + }, + }) + + s := &godap.LDAPServer{ + Handlers: hs, + } + + // start the LDAP server and wait for a short time to bring it up, + // connection would be refused otherwise + go s.ListenAndServe("127.0.0.99:10000") + time.Sleep(50 * time.Millisecond) +} + +func startHttpServer(ldapAuth auth.AuthenticatorInterface, addr string, timeout time.Duration) (*http.Server, *sync.WaitGroup) { + mux := http.NewServeMux() + + mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/" { + http.NotFound(w, req) + return + } + fmt.Fprintf(w, "Unrestricted") + }) + mux.HandleFunc("/secret", ldapAuthWrapper( + func(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "Logged in") + }, + ldapAuth, + )) + + srv := &http.Server{ + Addr: addr, + Handler: mux, + } + + log.Printf("Starting HTTP server on %s for %s", addr, timeout) + + httpServerExitDone := &sync.WaitGroup{} + httpServerExitDone.Add(1) + go func() { + defer httpServerExitDone.Done() + + // always returns error. ErrServerClosed on graceful close + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + log.Fatal("HTTP server error:", err) + } + }() + + // start the HTTP server and wait for a short time to bring it up, + // connection would be refused otherwise + time.Sleep(50 * time.Millisecond) + + return srv, httpServerExitDone +} + +func TestNewConnection(t *testing.T) { + cfg := loadFakeLdapConfig() + ldapAuthenticator, ldap_err := New(cfg.LDAP) + + if ldapAuthenticator != nil { + t.Fatal("No connection should be established to", cfg.LDAP.URL) + } + if ldap_err == nil { + t.Fatal("An error should raise while connecting to", cfg.LDAP.URL) + } + + startLdapServer() + + ldapAuthenticator, ldap_err = New(cfg.LDAP) + + if ldapAuthenticator == nil { + t.Fatal("Connection should be established to", cfg.LDAP.URL) + } + if ldap_err != nil { + t.Fatal("No error should raise while connecting to", cfg.LDAP.URL) + } + + // set an invalid bind password + cfg.LDAP.BindPassword = "asdf" + ldapAuthenticator, ldap_err = New(cfg.LDAP) + + if ldapAuthenticator != nil { + t.Fatal("No connection should be established with", cfg.LDAP.BindPassword) + } + if ldap_err == nil { + t.Fatal("An error should raise while connecting with", cfg.LDAP.BindPassword) + } +} + +func TestAuth(t *testing.T) { + cfg := loadFakeLdapConfig() + var ldapAuthenticator auth.AuthenticatorInterface + var ldap_err error + var httpServerAddr string = "127.0.0.99:4000" + var httpServerTimeout time.Duration = 5 * time.Second + // allow the onwards used user to successfully login + cfg.LDAP.UsernameAttribute = "pass" + + startLdapServer() + + ldapAuthenticator, ldap_err = New(cfg.LDAP) + + if ldapAuthenticator == nil { + t.Fatal("Connection should be established to", cfg.LDAP.URL) + } + if ldap_err != nil { + t.Fatal("No error should raise while connecting to", cfg.LDAP.URL) + } + + srv, httpServerExitDone := startHttpServer(ldapAuthenticator, httpServerAddr, httpServerTimeout) + + pageContent := crawlHttpPage("http://" + httpServerAddr) + if pageContent != "Unrestricted" { + t.Fatal("No content received from root page, expected 'Unrestricted'") + } + + securePageContent := crawlHttpPage("http://"+httpServerAddr+"/secret", "user", usersPasswords["user"]) + if securePageContent != "Logged in" { + t.Fatal("No content received from '/secret' page, expected 'Logged in'") + } + + // uncomment this sleep for manual testing and HTTP/LDAP interaction + // time.Sleep(60 * time.Second) + + if err := srv.Shutdown(context.Background()); err != nil { + log.Fatal("HTTP shutdown error:", err) + } + + // wait for started goroutine to stop + httpServerExitDone.Wait() + log.Println("HTTP server shutdown completed") +} + +func ldapAuthWrapper(handler http.HandlerFunc, authenticator auth.AuthenticatorInterface) http.HandlerFunc { + return auth.JustCheck(authenticator, handler) +} + +func crawlHttpPage(params ...string) string { + client := http.Client{Timeout: 1 * time.Second} + + req, err := http.NewRequest(http.MethodGet, params[0], http.NoBody) + if err != nil { + log.Fatal(err) + } + + if len(params) == 3 { + req.SetBasicAuth(params[1], params[2]) + } + + res, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + + defer res.Body.Close() + + resBody, err := io.ReadAll(res.Body) + if err != nil { + log.Fatal(err) + } + + return string(resBody) +} From f45c526bfbc9a020ce6ead81ba06d5e4cfa072b2 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Thu, 18 Apr 2024 11:35:57 +0200 Subject: [PATCH 4/5] Fixup nil pointer error --- config/config.go | 2 +- main.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/config.go b/config/config.go index 6cfc98604..8d8844652 100644 --- a/config/config.go +++ b/config/config.go @@ -385,7 +385,7 @@ func validateConfig(c *Config) error { "and 'tls_cert_file' specified.") } - if c.AllowUnauthenticatedReads && c.TLSCaFile == "" && c.HtpasswdFile == "" && c.LDAP.URL == "" { + if c.AllowUnauthenticatedReads && c.TLSCaFile == "" && c.HtpasswdFile == "" && c.LDAP == nil { return errors.New("AllowUnauthenticatedReads setting is only available when authentication is enabled") } diff --git a/main.go b/main.go index 2158058cd..060a94905 100644 --- a/main.go +++ b/main.go @@ -283,7 +283,7 @@ func startHttpServer(c *config.Config, httpServer **http.Server, statusHandler = h.VerifyClientCertHandler(statusHandler).ServeHTTP } else if c.HtpasswdFile != "" { statusHandler = basicAuthWrapper(statusHandler, &basicAuthenticator) - } else if c.LDAP.URL != "" { + } else if c.LDAP != nil { statusHandler = ldapAuthWrapper(statusHandler, ldapAuthenticator) } } @@ -303,7 +303,7 @@ func startHttpServer(c *config.Config, httpServer **http.Server, middlewareHandler = h.VerifyClientCertHandler(middlewareHandler) } else if c.HtpasswdFile != "" { middlewareHandler = basicAuthWrapper(middlewareHandler.ServeHTTP, &basicAuthenticator) - } else if c.LDAP.URL != "" { + } else if c.LDAP != nil { middlewareHandler = ldapAuthWrapper(middlewareHandler.ServeHTTP, ldapAuthenticator) } } From b8486d2cd33600a7b03ab9fbab6529c1e3a86ae7 Mon Sep 17 00:00:00 2001 From: Jonas Scharpf Date: Thu, 18 Apr 2024 11:44:53 +0200 Subject: [PATCH 5/5] Fixup spaces to tabs in ldap_test file --- ldap/ldap_test.go | 514 +++++++++++++++++++++++----------------------- 1 file changed, 257 insertions(+), 257 deletions(-) diff --git a/ldap/ldap_test.go b/ldap/ldap_test.go index 958223c49..098e01199 100644 --- a/ldap/ldap_test.go +++ b/ldap/ldap_test.go @@ -1,33 +1,33 @@ package ldap import ( - "context" - b64 "encoding/base64" - "fmt" - "io" - "log" - "net/http" - "strings" - "sync" - "testing" - "time" - - "github.com/JonasScharpf/godap/godap" - simplesearch "github.com/JonasScharpf/godap/godap" - "github.com/abbot/go-http-auth" - config "github.com/buchgr/bazel-remote/v2/config" + "context" + b64 "encoding/base64" + "fmt" + "io" + "log" + "net/http" + "strings" + "sync" + "testing" + "time" + + "github.com/JonasScharpf/godap/godap" + simplesearch "github.com/JonasScharpf/godap/godap" + "github.com/abbot/go-http-auth" + config "github.com/buchgr/bazel-remote/v2/config" ) func loadYamlConfig(data []byte) *config.Config { - cfg, err := config.NewConfigFromYaml(data) - if err != nil { - log.Fatal(err) - } - return cfg + cfg, err := config.NewConfigFromYaml(data) + if err != nil { + log.Fatal(err) + } + return cfg } func loadFakeLdapConfig() *config.Config { - yaml := `host: localhost + yaml := `host: localhost port: 8080 dir: /opt/cache-dir max_size: 100 @@ -43,270 +43,270 @@ ldap: - CN=other-users,OU=Groups2,OU=Alien Users,DC=foo,DC=org ` - return loadYamlConfig([]byte(yaml)) + return loadYamlConfig([]byte(yaml)) } var usersPasswords = map[string]string{ - "CN=read-only-admin,OU=My Users,DC=example,DC=com": "1234", - "user": "password", - "cn=user,OU=My Users,DC=example,DC=com": "password", + "CN=read-only-admin,OU=My Users,DC=example,DC=com": "1234", + "user": "password", + "cn=user,OU=My Users,DC=example,DC=com": "password", } func verifyUserPass(username string, password string) bool { - log.Printf("Looking for username '%s' with password '%s'", username, password) - wantPass, hasUser := usersPasswords[username] - if !hasUser { - log.Printf("No such user '%s'", username) - return false - } - if wantPass == password { - log.Println("Password and username are valid") - return true - } - log.Printf("Invalid password for username '%s'", username) - return false + log.Printf("Looking for username '%s' with password '%s'", username, password) + wantPass, hasUser := usersPasswords[username] + if !hasUser { + log.Printf("No such user '%s'", username) + return false + } + if wantPass == password { + log.Println("Password and username are valid") + return true + } + log.Printf("Invalid password for username '%s'", username) + return false } func startLdapServer() { - hs := make([]godap.LDAPRequestHandler, 0) - - // use a LDAPBindFuncHandler to provide a callback function to respond - // to bind requests - hs = append(hs, &godap.LDAPBindFuncHandler{ - LDAPBindFunc: func(binddn string, bindpw []byte) bool { - return verifyUserPass(binddn, string(bindpw)) - }, - }) - - // use a LDAPSimpleSearchFuncHandler to reply to search queries - hs = append(hs, &simplesearch.LDAPSimpleSearchFuncHandler{ - LDAPSimpleSearchFunc: func(req *godap.LDAPSimpleSearchRequest) []*godap.LDAPSimpleSearchResultEntry { - ret := make([]*godap.LDAPSimpleSearchResultEntry, 0, 1) - - if req.FilterAttr == "uid" { - userPassword := b64.StdEncoding.EncodeToString([]byte(req.FilterValue)) - - ret = append(ret, &simplesearch.LDAPSimpleSearchResultEntry{ - DN: "cn=" + req.FilterValue + "," + req.BaseDN, - Attrs: map[string]interface{}{ - "cn": req.FilterValue, - "sn": req.FilterValue, - "uid": req.FilterValue, - "userPassword": userPassword, - "homeDirectory": "/home/" + req.FilterValue, - "objectClass": []string{ - "top", - "posixAccount", - "inetOrgPerson", - }, - }, - Skip: false, - }) - } else if req.FilterAttr == "searchFingerprint" { - // a non-simple search request has been received and should be - // processed. For simplicity, as this is just a fake LDAP - // server simple but really bad assumptions are done onwards. - // If the first query element is "pass" a response is sent, - // otherwise not. By this a user can be available/found or not - filterValues := strings.Split(req.FilterValue, ";") - passOrFail := filterValues[0] - user := filterValues[1] - userPassword := b64.StdEncoding.EncodeToString([]byte(user)) - // TODO add user with this password to the mapping - - if passOrFail == "pass" { - log.Println("Simulate 'query match'") - ret = append(ret, &simplesearch.LDAPSimpleSearchResultEntry{ - DN: "cn=" + user + "," + req.BaseDN, - Attrs: map[string]interface{}{ - "cn": user, - "sn": user, - "uid": user, - "userPassword": userPassword, - "homeDirectory": "/home/" + user, - "objectClass": []string{ - "top", - "posixAccount", - "inetOrgPerson", - }, - }, - Skip: false, - }) - } else { - log.Println("Simulate 'no query match'") - // "skip" this one in the LDAP processing step to mock an - // empty response - ret = append(ret, &simplesearch.LDAPSimpleSearchResultEntry{ - DN: "cn=" + user + "," + req.BaseDN, - Attrs: map[string]interface{}{ - "cn": user, - }, - Skip: true, - }) - } - } - - return ret - }, - }) - - s := &godap.LDAPServer{ - Handlers: hs, - } - - // start the LDAP server and wait for a short time to bring it up, - // connection would be refused otherwise - go s.ListenAndServe("127.0.0.99:10000") - time.Sleep(50 * time.Millisecond) + hs := make([]godap.LDAPRequestHandler, 0) + + // use a LDAPBindFuncHandler to provide a callback function to respond + // to bind requests + hs = append(hs, &godap.LDAPBindFuncHandler{ + LDAPBindFunc: func(binddn string, bindpw []byte) bool { + return verifyUserPass(binddn, string(bindpw)) + }, + }) + + // use a LDAPSimpleSearchFuncHandler to reply to search queries + hs = append(hs, &simplesearch.LDAPSimpleSearchFuncHandler{ + LDAPSimpleSearchFunc: func(req *godap.LDAPSimpleSearchRequest) []*godap.LDAPSimpleSearchResultEntry { + ret := make([]*godap.LDAPSimpleSearchResultEntry, 0, 1) + + if req.FilterAttr == "uid" { + userPassword := b64.StdEncoding.EncodeToString([]byte(req.FilterValue)) + + ret = append(ret, &simplesearch.LDAPSimpleSearchResultEntry{ + DN: "cn=" + req.FilterValue + "," + req.BaseDN, + Attrs: map[string]interface{}{ + "cn": req.FilterValue, + "sn": req.FilterValue, + "uid": req.FilterValue, + "userPassword": userPassword, + "homeDirectory": "/home/" + req.FilterValue, + "objectClass": []string{ + "top", + "posixAccount", + "inetOrgPerson", + }, + }, + Skip: false, + }) + } else if req.FilterAttr == "searchFingerprint" { + // a non-simple search request has been received and should be + // processed. For simplicity, as this is just a fake LDAP + // server simple but really bad assumptions are done onwards. + // If the first query element is "pass" a response is sent, + // otherwise not. By this a user can be available/found or not + filterValues := strings.Split(req.FilterValue, ";") + passOrFail := filterValues[0] + user := filterValues[1] + userPassword := b64.StdEncoding.EncodeToString([]byte(user)) + // TODO add user with this password to the mapping + + if passOrFail == "pass" { + log.Println("Simulate 'query match'") + ret = append(ret, &simplesearch.LDAPSimpleSearchResultEntry{ + DN: "cn=" + user + "," + req.BaseDN, + Attrs: map[string]interface{}{ + "cn": user, + "sn": user, + "uid": user, + "userPassword": userPassword, + "homeDirectory": "/home/" + user, + "objectClass": []string{ + "top", + "posixAccount", + "inetOrgPerson", + }, + }, + Skip: false, + }) + } else { + log.Println("Simulate 'no query match'") + // "skip" this one in the LDAP processing step to mock an + // empty response + ret = append(ret, &simplesearch.LDAPSimpleSearchResultEntry{ + DN: "cn=" + user + "," + req.BaseDN, + Attrs: map[string]interface{}{ + "cn": user, + }, + Skip: true, + }) + } + } + + return ret + }, + }) + + s := &godap.LDAPServer{ + Handlers: hs, + } + + // start the LDAP server and wait for a short time to bring it up, + // connection would be refused otherwise + go s.ListenAndServe("127.0.0.99:10000") + time.Sleep(50 * time.Millisecond) } func startHttpServer(ldapAuth auth.AuthenticatorInterface, addr string, timeout time.Duration) (*http.Server, *sync.WaitGroup) { - mux := http.NewServeMux() - - mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { - if req.URL.Path != "/" { - http.NotFound(w, req) - return - } - fmt.Fprintf(w, "Unrestricted") - }) - mux.HandleFunc("/secret", ldapAuthWrapper( - func(w http.ResponseWriter, req *http.Request) { - fmt.Fprintf(w, "Logged in") - }, - ldapAuth, - )) - - srv := &http.Server{ - Addr: addr, - Handler: mux, - } - - log.Printf("Starting HTTP server on %s for %s", addr, timeout) - - httpServerExitDone := &sync.WaitGroup{} - httpServerExitDone.Add(1) - go func() { - defer httpServerExitDone.Done() - - // always returns error. ErrServerClosed on graceful close - if err := srv.ListenAndServe(); err != http.ErrServerClosed { - log.Fatal("HTTP server error:", err) - } - }() - - // start the HTTP server and wait for a short time to bring it up, - // connection would be refused otherwise - time.Sleep(50 * time.Millisecond) - - return srv, httpServerExitDone + mux := http.NewServeMux() + + mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/" { + http.NotFound(w, req) + return + } + fmt.Fprintf(w, "Unrestricted") + }) + mux.HandleFunc("/secret", ldapAuthWrapper( + func(w http.ResponseWriter, req *http.Request) { + fmt.Fprintf(w, "Logged in") + }, + ldapAuth, + )) + + srv := &http.Server{ + Addr: addr, + Handler: mux, + } + + log.Printf("Starting HTTP server on %s for %s", addr, timeout) + + httpServerExitDone := &sync.WaitGroup{} + httpServerExitDone.Add(1) + go func() { + defer httpServerExitDone.Done() + + // always returns error. ErrServerClosed on graceful close + if err := srv.ListenAndServe(); err != http.ErrServerClosed { + log.Fatal("HTTP server error:", err) + } + }() + + // start the HTTP server and wait for a short time to bring it up, + // connection would be refused otherwise + time.Sleep(50 * time.Millisecond) + + return srv, httpServerExitDone } func TestNewConnection(t *testing.T) { - cfg := loadFakeLdapConfig() - ldapAuthenticator, ldap_err := New(cfg.LDAP) - - if ldapAuthenticator != nil { - t.Fatal("No connection should be established to", cfg.LDAP.URL) - } - if ldap_err == nil { - t.Fatal("An error should raise while connecting to", cfg.LDAP.URL) - } - - startLdapServer() - - ldapAuthenticator, ldap_err = New(cfg.LDAP) - - if ldapAuthenticator == nil { - t.Fatal("Connection should be established to", cfg.LDAP.URL) - } - if ldap_err != nil { - t.Fatal("No error should raise while connecting to", cfg.LDAP.URL) - } - - // set an invalid bind password - cfg.LDAP.BindPassword = "asdf" - ldapAuthenticator, ldap_err = New(cfg.LDAP) - - if ldapAuthenticator != nil { - t.Fatal("No connection should be established with", cfg.LDAP.BindPassword) - } - if ldap_err == nil { - t.Fatal("An error should raise while connecting with", cfg.LDAP.BindPassword) - } + cfg := loadFakeLdapConfig() + ldapAuthenticator, ldap_err := New(cfg.LDAP) + + if ldapAuthenticator != nil { + t.Fatal("No connection should be established to", cfg.LDAP.URL) + } + if ldap_err == nil { + t.Fatal("An error should raise while connecting to", cfg.LDAP.URL) + } + + startLdapServer() + + ldapAuthenticator, ldap_err = New(cfg.LDAP) + + if ldapAuthenticator == nil { + t.Fatal("Connection should be established to", cfg.LDAP.URL) + } + if ldap_err != nil { + t.Fatal("No error should raise while connecting to", cfg.LDAP.URL) + } + + // set an invalid bind password + cfg.LDAP.BindPassword = "asdf" + ldapAuthenticator, ldap_err = New(cfg.LDAP) + + if ldapAuthenticator != nil { + t.Fatal("No connection should be established with", cfg.LDAP.BindPassword) + } + if ldap_err == nil { + t.Fatal("An error should raise while connecting with", cfg.LDAP.BindPassword) + } } func TestAuth(t *testing.T) { - cfg := loadFakeLdapConfig() - var ldapAuthenticator auth.AuthenticatorInterface - var ldap_err error - var httpServerAddr string = "127.0.0.99:4000" - var httpServerTimeout time.Duration = 5 * time.Second - // allow the onwards used user to successfully login - cfg.LDAP.UsernameAttribute = "pass" - - startLdapServer() - - ldapAuthenticator, ldap_err = New(cfg.LDAP) - - if ldapAuthenticator == nil { - t.Fatal("Connection should be established to", cfg.LDAP.URL) - } - if ldap_err != nil { - t.Fatal("No error should raise while connecting to", cfg.LDAP.URL) - } - - srv, httpServerExitDone := startHttpServer(ldapAuthenticator, httpServerAddr, httpServerTimeout) - - pageContent := crawlHttpPage("http://" + httpServerAddr) - if pageContent != "Unrestricted" { - t.Fatal("No content received from root page, expected 'Unrestricted'") - } - - securePageContent := crawlHttpPage("http://"+httpServerAddr+"/secret", "user", usersPasswords["user"]) - if securePageContent != "Logged in" { - t.Fatal("No content received from '/secret' page, expected 'Logged in'") - } - - // uncomment this sleep for manual testing and HTTP/LDAP interaction - // time.Sleep(60 * time.Second) - - if err := srv.Shutdown(context.Background()); err != nil { - log.Fatal("HTTP shutdown error:", err) - } - - // wait for started goroutine to stop - httpServerExitDone.Wait() - log.Println("HTTP server shutdown completed") + cfg := loadFakeLdapConfig() + var ldapAuthenticator auth.AuthenticatorInterface + var ldap_err error + var httpServerAddr string = "127.0.0.99:4000" + var httpServerTimeout time.Duration = 5 * time.Second + // allow the onwards used user to successfully login + cfg.LDAP.UsernameAttribute = "pass" + + startLdapServer() + + ldapAuthenticator, ldap_err = New(cfg.LDAP) + + if ldapAuthenticator == nil { + t.Fatal("Connection should be established to", cfg.LDAP.URL) + } + if ldap_err != nil { + t.Fatal("No error should raise while connecting to", cfg.LDAP.URL) + } + + srv, httpServerExitDone := startHttpServer(ldapAuthenticator, httpServerAddr, httpServerTimeout) + + pageContent := crawlHttpPage("http://" + httpServerAddr) + if pageContent != "Unrestricted" { + t.Fatal("No content received from root page, expected 'Unrestricted'") + } + + securePageContent := crawlHttpPage("http://"+httpServerAddr+"/secret", "user", usersPasswords["user"]) + if securePageContent != "Logged in" { + t.Fatal("No content received from '/secret' page, expected 'Logged in'") + } + + // uncomment this sleep for manual testing and HTTP/LDAP interaction + // time.Sleep(60 * time.Second) + + if err := srv.Shutdown(context.Background()); err != nil { + log.Fatal("HTTP shutdown error:", err) + } + + // wait for started goroutine to stop + httpServerExitDone.Wait() + log.Println("HTTP server shutdown completed") } func ldapAuthWrapper(handler http.HandlerFunc, authenticator auth.AuthenticatorInterface) http.HandlerFunc { - return auth.JustCheck(authenticator, handler) + return auth.JustCheck(authenticator, handler) } func crawlHttpPage(params ...string) string { - client := http.Client{Timeout: 1 * time.Second} + client := http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest(http.MethodGet, params[0], http.NoBody) - if err != nil { - log.Fatal(err) - } + req, err := http.NewRequest(http.MethodGet, params[0], http.NoBody) + if err != nil { + log.Fatal(err) + } - if len(params) == 3 { - req.SetBasicAuth(params[1], params[2]) - } + if len(params) == 3 { + req.SetBasicAuth(params[1], params[2]) + } - res, err := client.Do(req) - if err != nil { - log.Fatal(err) - } + res, err := client.Do(req) + if err != nil { + log.Fatal(err) + } - defer res.Body.Close() + defer res.Body.Close() - resBody, err := io.ReadAll(res.Body) - if err != nil { - log.Fatal(err) - } + resBody, err := io.ReadAll(res.Body) + if err != nil { + log.Fatal(err) + } - return string(resBody) + return string(resBody) }