diff --git a/.bazelci/ldap-tests.sh b/.bazelci/ldap-tests.sh new file mode 100755 index 000000000..bb3e6c83c --- /dev/null +++ b/.bazelci/ldap-tests.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +set -v +set -e +set -u +set -o pipefail + +SRC_ROOT=$(dirname "$0")/.. +SRC_ROOT=$(realpath "$SRC_ROOT") +cd "$SRC_ROOT" + +HTTP_PORT=8089 + +[ -f bazel-remote ] || ./linux-build.sh + +[ -f glauth-linux-amd64 ] || wget https://github.com/glauth/glauth/releases/download/v2.3.2/glauth-linux-amd64 +chmod +x glauth-linux-amd64 + +tmpdir=$(mktemp -d bazel-remote-ldap-tests.XXXXXXX --tmpdir=${TMPDIR:-/tmp}) +cd $tmpdir + +BIND_USER_NAME="bazel-remote-ldap-user" +BIND_USER_PASSWORD="bazel-remote-ldap-password" +BIND_USER_PASSWORD_HASH=$(echo -n "$BIND_USER_PASSWORD" | sha256sum | cut -d' ' -f1) + +END_USER_NAME="user-name" +END_USER_PASSWORD="user-password" +END_USER_PASSWORD_HASH=$(echo -n "$END_USER_PASSWORD" | sha256sum | cut -d' ' -f1) + +# Based on https://github.com/glauth/glauth/blob/master/v2/sample-simple.cfg +cat << EOF > glauth.config +[ldap] + enabled = true + listen = "0.0.0.0:3893" + tls = false + +[ldaps] + enabled = false + +[tracing] + enabled = false + +[backend] + datastore = "config" + baseDN = "dc=glauth,dc=com" + nameformat = "cn" + groupformat = "ou" + +# The users section contains a hardcoded list of valid users. +# to create a passSHA256: echo -n "mysecret" | openssl dgst -sha256 + +[[users]] + name = "$BIND_USER_NAME" + uidnumber = 5003 + primarygroup = 5502 + passsha256 = "$BIND_USER_PASSWORD_HASH" + [[users.capabilities]] + action = "search" + object = "*" + +[[users]] + name = "$END_USER_NAME" + uidnumber = 5001 + primarygroup = 5501 + passsha256 = "$END_USER_PASSWORD_HASH" + [[users.capabilities]] + action = "search" + object = "ou=superheros,dc=glauth,dc=com" + +EOF + +"$SRC_ROOT/glauth-linux-amd64" -c glauth.config & +glauth_pid=$! +sleep 5 + +"$SRC_ROOT/bazel-remote" --dir data --max_size 1 --http_address "0.0.0.0:$HTTP_PORT" \ + --enable_endpoint_metrics \ + --ldap.url ldap://127.0.0.1:3893 \ + --ldap.base_dn dc=glauth,dc=com \ + --ldap.bind_user "$BIND_USER_NAME" \ + --ldap.bind_password "$BIND_USER_PASSWORD" & +bazel_remote_pid=$! + +# Wait a bit for bazel-remote to start up... + +running=false +for i in $(seq 1 20) +do + sleep 1 + + ps -p $bazel_remote_pid > /dev/null || break + + if wget --inet4-only -d -O - --timeout=2 \ + --http-user "$END_USER_NAME" --http-password "$END_USER_PASSWORD" \ + "http://127.0.0.1:$HTTP_PORT/status" + then + running=true + break + fi +done + +if [ "$running" != true ] +then + echo "Error: bazel-remote took too long to start" + kill -9 $bazel_remote_pid $glauth_pid + exit 1 +fi + +# Check that metrics are reachable with authentication. +wget --inet4-only -d -O - \ + --http-user "$END_USER_NAME" --http-password "$END_USER_PASSWORD" \ + http://127.0.0.1:$HTTP_PORT/metrics 2>&1 | tee authenticated_metrics.log + +# Check that metrics are not reachable without authentication. +set +e +wget --inet4-only -d -O - \ + http://127.0.0.1:$HTTP_PORT/metrics > unauthenticated_metrics.log 2>&1 +result=$? +if [ $result = 0 ] +then + cat unauthenticated_metrics.log + echo Error: should not have been able to fetch metrics without authentication. + exit 1 +fi +set -e + + +echo LDAP tests passed, cleaning up... +kill -9 $bazel_remote_pid $glauth_pid +cd "$SRC_ROOT" +rm -rf "$tmpdir" diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 175321845..0afee6d45 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -83,6 +83,13 @@ tasks: - ".bazelci/buildkite-install-go.sh" - "echo +++ Run basic auth tests" - "PATH=$HOME/go/bin:$PATH timeout 30m .bazelci/basic-auth-tests.sh" + ldap_tests: + platform: ubuntu2004 + name: "LDAP tests" + shell_commands: + - ".bazelci/buildkite-install-go.sh" + - "echo +++ Run LDAP tests" + - "PATH=$HOME/go/bin:$PATH timeout 30m .bazelci/ldap-tests.sh" migration_tests: platform: ubuntu2004 name: "migration tests" 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 16c07c278..2756d4ebd 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,30 @@ 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 also supported. Note that this feature is currently considered + experimental. [$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_query value Filter clause for searching groups. + [$BAZEL_REMOTE_LDAP_GROUPS_QUERY] + + --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] @@ -469,7 +493,15 @@ 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_query: (memberOf=CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com) # If tls_ca_file or htpasswd_file are specified, you can choose # whether or not to allow unauthenticated read access: @@ -679,7 +711,12 @@ $ bazel build :bazel-remote ### Authentication 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 (experimental) LDAP are +also supported. + +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 @@ -698,6 +735,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 @@ -716,6 +755,27 @@ $ docker run -v /path/to/cache/dir:/data \ --max_size 5 ``` +#### LDAP + +There is also an experimental LDAP authentication method. A configuration +file is advised to avoid leaking the ldap.bind_password value to local +users, but command line arguments are also supported. + +Note that the configuration options for this feature might change while +the feature is still considered "experimental". + +```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_query="(|(memberOf=CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com)(memberOf=CN=other-users,OU=Groups2,OU=Alien Users,DC=foo,DC=org))" \ + --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 The following demonstrates how to configure a docker instance of bazel-remote to use an AWS S3 diff --git a/WORKSPACE b/WORKSPACE index 10fff226e..c9b260bfd 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -124,6 +124,13 @@ go_repository( version = "v1.3.5", ) +go_repository( + name = "in_gopkg_asn1_ber_v1", + importpath = "gopkg.in/asn1-ber.v1", + sum = "h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=", + version = "v1.0.0-20181015200546-f715ec2f112d", +) + gazelle_dependencies() http_archive( diff --git a/config/config.go b/config/config.go index 51655d00d..832ce6780 100644 --- a/config/config.go +++ b/config/config.go @@ -38,6 +38,16 @@ type URLBackendConfig struct { CaFile string `yaml:"ca_file"` } +type LDAPConfig struct { + URL string `yaml:"url"` + BaseDN string `yaml:"base_dn"` + BindUser string `yaml:"bind_user"` + BindPassword string `yaml:"bind_password"` + UsernameAttribute string `yaml:"username_attribute"` + GroupsQuery string `yaml:"groups_query"` + CacheTime time.Duration `yaml:"cache_time"` +} + func (c *URLBackendConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { type Aux URLBackendConfig aux := &struct { @@ -89,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"` @@ -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, @@ -229,10 +242,10 @@ func newFromYamlFile(path string) (*Config, error) { return nil, fmt.Errorf("Failed to read config file '%s': %v", path, err) } - return newFromYaml(data) + return NewFromYaml(data) } -func newFromYaml(data []byte) (*Config, error) { +func NewFromYaml(data []byte) (*Config, error) { yc := YamlConfig{ Config: Config{ StorageMode: "zstd", @@ -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 == nil { return errors.New("AllowUnauthenticatedReads setting is only available when authentication is enabled") } @@ -450,6 +463,25 @@ func validateConfig(c *Config) error { } } + if c.HtpasswdFile != "" && c.TLSCaFile != "" && c.LDAP != nil { + return errors.New("One can specify at most one authentication mechanism") + } + + if c.LDAP != nil { + if c.LDAP.URL == "" { + 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 +622,19 @@ func get(ctx *cli.Context) (*Config, error) { } } + var ldap *LDAPConfig + if ctx.String("ldap.url") != "" { + ldap = &LDAPConfig{ + URL: 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"), + GroupsQuery: ctx.String("ldap.groups_query"), + CacheTime: ctx.Duration("ldap.cache_time"), + } + } + return newFromArgs( ctx.String("dir"), ctx.Int("max_size"), @@ -610,6 +655,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..9fc4cce60 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -31,7 +31,7 @@ access_log_level: none log_timezone: local ` - config, err := newFromYaml([]byte(yaml)) + config, err := NewFromYaml([]byte(yaml)) if err != nil { t.Fatal(err) } @@ -78,7 +78,7 @@ gcs_proxy: use_default_credentials: false json_credentials_file: /opt/creds.json ` - config, err := newFromYaml([]byte(yaml)) + config, err := NewFromYaml([]byte(yaml)) if err != nil { t.Fatal(err) } @@ -120,7 +120,7 @@ http_proxy: url: https://remote-cache.com:8080/cache mode: zstd ` - config, err := newFromYaml([]byte(yaml)) + config, err := NewFromYaml([]byte(yaml)) if err != nil { t.Fatal(err) } @@ -197,7 +197,7 @@ s3_proxy: access_key_id: EXAMPLE_ACCESS_KEY secret_access_key: EXAMPLE_SECRET_KEY ` - config, err := newFromYaml([]byte(yaml)) + config, err := NewFromYaml([]byte(yaml)) if err != nil { t.Fatal(err) } @@ -231,6 +231,55 @@ 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_query: (|(memberOf=CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com)(memberOf=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{ + URL: "ldap://ldap.example.com", + BaseDN: "OU=My Users,DC=example,DC=com", + BindUser: "ldapuser", + BindPassword: "ldappassword", + UsernameAttribute: "sAMAccountName", + GroupsQuery: "(|(memberOf=CN=bazel-users,OU=Groups,OU=My Users,DC=example,DC=com)(memberOf=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 @@ -238,7 +287,7 @@ dir: /opt/cache-dir max_size: 42 profile_address: :7070 ` - config, err := newFromYaml([]byte(yaml)) + config, err := NewFromYaml([]byte(yaml)) if err != nil { t.Fatal(err) } @@ -269,7 +318,7 @@ profile_address: :7070 expectedConfig.ProfileAddress = "192.168.1.1:7070" - config, err = newFromYaml([]byte(yaml)) + config, err = NewFromYaml([]byte(yaml)) if err != nil { t.Fatal(err) } @@ -287,7 +336,7 @@ max_size: 42 storage_mode: zstd endpoint_metrics_duration_buckets: [.005, .1, 5] ` - config, err := newFromYaml([]byte(yaml)) + config, err := NewFromYaml([]byte(yaml)) if err != nil { t.Fatal(err) } @@ -375,7 +424,7 @@ storage_mode: gzip }} for _, tc := range tests { - cfg, err := newFromYaml([]byte(tc.yaml)) + cfg, err := NewFromYaml([]byte(tc.yaml)) if !tc.invalid && err != nil { t.Error("Expected to succeed, got", err) } @@ -418,7 +467,7 @@ dir: /opt/cache-dir max_size: 42 storage_mode: zstd ` - config, err := newFromYaml([]byte(yaml)) + config, err := NewFromYaml([]byte(yaml)) if err != nil { t.Fatal(err) } @@ -452,7 +501,7 @@ dir: /opt/cache-dir max_size: 42 storage_mode: zstd ` - config, err := newFromYaml([]byte(yaml)) + config, err := NewFromYaml([]byte(yaml)) if err != nil { t.Fatal(err) } diff --git a/deps.bzl b/deps.bzl index 8c1e21205..0a8ae1b21 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", @@ -231,6 +245,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", @@ -244,6 +265,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", @@ -304,9 +331,16 @@ def go_dependencies(): go_repository( name = "com_github_golang_jwt_jwt", importpath = "github.com/golang-jwt/jwt", - sum = "h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c=", - version = "v3.2.1+incompatible", + sum = "h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=", + version = "v3.2.2+incompatible", ) + go_repository( + name = "com_github_golang_jwt_jwt_v4", + importpath = "github.com/golang-jwt/jwt/v4", + sum = "h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=", + version = "v4.5.0", + ) + go_repository( name = "com_github_golang_jwt_jwt_v5", importpath = "github.com/golang-jwt/jwt/v5", @@ -384,6 +418,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", @@ -391,18 +437,63 @@ 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_josharian_intern", importpath = "github.com/josharian/intern", @@ -1603,6 +1694,12 @@ def go_dependencies(): sum = "h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=", version = "v1.67.0", ) + go_repository( + name = "in_gopkg_ldap_v3", + importpath = "gopkg.in/ldap.v3", + sum = "h1:YKRHW/2sIl05JsCtx/5ZuUueFuJyoj/6+DGXe3wp6ro=", + version = "v3.0.3", + ) go_repository( name = "in_gopkg_yaml_v2", diff --git a/go.mod b/go.mod index 304c99704..9bac0ea4e 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,8 @@ require ( google.golang.org/genproto v0.0.0-20240401170217-c3f982113cda google.golang.org/grpc v1.64.0 google.golang.org/protobuf v1.34.1 + gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect + gopkg.in/ldap.v3 v3.0.3 gopkg.in/yaml.v3 v3.0.1 ) @@ -29,6 +31,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.2 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.4.1 + 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-20240401170217-c3f982113cda @@ -42,12 +45,16 @@ require ( cloud.google.com/go/longrunning v0.5.6 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect + github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // 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.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/golang-jwt/jwt/v5 v5.2.1 // 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.7 // indirect @@ -70,6 +77,7 @@ require ( golang.org/x/net v0.24.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.8.0 // 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 015b0b220..8bdf69757 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,13 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2 h1:LqbJ/WzJUwBf8UiaSzgX7aM github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc= 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 v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 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-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= @@ -34,6 +37,13 @@ github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 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.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.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/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -45,8 +55,18 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +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= @@ -107,10 +127,16 @@ 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.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.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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= @@ -123,17 +149,25 @@ 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.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.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= @@ -152,6 +186,9 @@ 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.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= @@ -159,6 +196,9 @@ 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.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= @@ -187,15 +227,18 @@ google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +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/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 new file mode 100644 index 000000000..190c0ee6e --- /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", + "@com_github_go_ldap_ldap_v3//:go_default_library", + ], +) diff --git a/ldap/ldap.go b/ldap/ldap.go new file mode 100644 index 000000000..e12a004a0 --- /dev/null +++ b/ldap/ldap.go @@ -0,0 +1,160 @@ +package ldap + +import ( + "context" + "encoding/base64" + "fmt" + "log" + "net/http" + "strings" + "sync" + "time" + + "github.com/buchgr/bazel-remote/v2/config" + + auth "github.com/abbot/go-http-auth" + ldap "github.com/go-ldap/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.URL) + if err != nil { + return nil, err + } + defer conn.Close() + + // Test the configured bind credentials + if err = conn.Bind(config.BindUser+","+config.BaseDN, 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.URL) + if err != nil { + log.Fatal("No valid LDAP connection could be established:", err) + } + defer conn.Close() + + if err = conn.Bind(c.config.BindUser+","+c.config.BaseDN, c.config.BindPassword); err != nil { + log.Fatal("LDAP connection with username and password failed:", err) + } + + query := fmt.Sprintf("(&(%s=%s)%s)", c.config.UsernameAttribute, user, c.config.GroupsQuery) + + 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..6ecd4fb07 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" @@ -242,6 +243,7 @@ func startHttpServer(c *config.Config, httpServer **http.Server, c.EnableACKeyInstanceMangling, checkClientCertForReads, checkClientCertForWrites, gitCommit) cacheHandler := h.CacheHandler + var ldapAuthenticator authenticator var basicAuthenticator auth.BasicAuth if c.HtpasswdFile != "" { if c.AllowUnauthenticatedReads { @@ -250,6 +252,16 @@ func startHttpServer(c *config.Config, httpServer **http.Server, basicAuthenticator = auth.BasicAuth{Realm: c.HTTPAddress, Secrets: htpasswdSecrets} cacheHandler = basicAuthWrapper(cacheHandler, &basicAuthenticator) } + } else if c.LDAP != nil { + 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) + } } if c.IdleTimeout > 0 { @@ -267,6 +279,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 != nil { + statusHandler = ldapAuthWrapper(statusHandler, ldapAuthenticator) } } @@ -285,6 +299,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 != nil { + middlewareHandler = ldapAuthWrapper(middlewareHandler.ServeHTTP, ldapAuthenticator) } } mux.Handle("/metrics", middlewareHandler) @@ -432,12 +448,21 @@ func startGrpcServer(c *config.Config, grpcServer **grpc.Server, diskCache, c.AccessLogger, c.ErrorLogger) } +type authenticator interface { + NewContext(ctx context.Context, r *http.Request) context.Context + Wrap(auth.AuthenticatedHandlerFunc) http.HandlerFunc +} + // A http.HandlerFunc wrapper which requires successful basic // authentication for all requests. func basicAuthWrapper(handler http.HandlerFunc, authenticator *auth.BasicAuth) http.HandlerFunc { return auth.JustCheck(authenticator, handler) } +func ldapAuthWrapper(handler http.HandlerFunc, authenticator authenticator) 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..a385bd549 100644 --- a/utils/flags/flags.go +++ b/utils/flags/flags.go @@ -254,6 +254,48 @@ 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 also supported. Note that this feature is currently considered experimental.", + 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"}, + }, + // to allow anonymous access do not require BindUser or BindPassword + &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"}, + }, + &cli.StringFlag{ + Name: "ldap.groups_query", + Usage: "Filter clause for searching groups.", + EnvVars: []string{"BAZEL_REMOTE_LDAP_GROUPS_QUERY"}, + }, + &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: "",