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

Land initial LDAP support #768

Merged
merged 11 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
131 changes: 131 additions & 0 deletions .bazelci/ldap-tests.sh
Original file line number Diff line number Diff line change
@@ -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"
7 changes: 7 additions & 0 deletions .bazelci/presubmit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
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
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
52 changes: 49 additions & 3 deletions 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 {
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 {
Expand Down Expand Up @@ -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"`
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 @@ -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",
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 == nil {
return errors.New("AllowUnauthenticatedReads setting is only available when authentication is enabled")
}

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"),
Expand All @@ -610,6 +655,7 @@ func get(ctx *cli.Context) (*Config, error) {
hc,
grpcb,
gcs,
ldap,
s3,
azblob,
ctx.Bool("disable_http_ac_validation"),
Expand Down
Loading
Loading