Skip to content

Commit

Permalink
Support LDAP authentication with unauthorized reads
Browse files Browse the repository at this point in the history
  • Loading branch information
JonasScharpf committed Apr 2, 2024
1 parent ee1e146 commit faecd41
Show file tree
Hide file tree
Showing 11 changed files with 438 additions and 4 deletions.
1 change: 1 addition & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
52 changes: 49 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
49 changes: 48 additions & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
}

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"),
Expand All @@ -610,6 +656,7 @@ func get(ctx *cli.Context) (*Config, error) {
hc,
grpcb,
gcs,
ldap,
s3,
azblob,
ctx.Bool("disable_http_ac_validation"),
Expand Down
51 changes: 51 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand All @@ -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
Expand Down
Loading

0 comments on commit faecd41

Please sign in to comment.