Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support LDAP authentication with unauthorized reads #740

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
JonasScharpf marked this conversation as resolved.
Show resolved Hide resolved
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:
JonasScharpf marked this conversation as resolved.
Show resolved Hide resolved
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)
JonasScharpf marked this conversation as resolved.
Show resolved Hide resolved
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"`
JonasScharpf marked this conversation as resolved.
Show resolved Hide resolved
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,
JonasScharpf marked this conversation as resolved.
Show resolved Hide resolved
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 {
JonasScharpf marked this conversation as resolved.
Show resolved Hide resolved
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
JonasScharpf marked this conversation as resolved.
Show resolved Hide resolved
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
Loading