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

[MVP]integrate LDAP as external auth service #2

Merged
merged 17 commits into from
May 31, 2023
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: build ci

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.19

- name: build
run: make build
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need run tests.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, need a better way to test other modes, now we could only test with simple static configuration.
we should test with different configuration modes, eventually, not a very high priority.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will fix this later.

29 changes: 29 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.PHONY: build build-local run test
build:
docker run --rm -v `pwd`:/go/src/go-filter -w /go/src/go-filter \
-e GOPROXY=https://goproxy.cn \
golang:1.19 \
go build -v -o libgolang.so -buildmode=c-shared -buildvcs=false .

build-local:
GOPROXY=https://goproxy.cn go build -v -o libgolang.so -buildmode=c-shared .

run:
docker run --rm -v `pwd`/envoy.yaml:/etc/envoy/envoy.yaml \
-v `pwd`/libgolang.so:/etc/envoy/libgolang.so \
-p 10000:10000 \
envoyproxy/envoy:contrib-dev \
envoy -c /etc/envoy/envoy.yaml

test-run:
docker run --rm -v `pwd`/example/envoy.yaml:/etc/envoy/envoy.yaml \
-v `pwd`/libgolang.so:/etc/envoy/libgolang.so \
-p 10000:10000 \
envoyproxy/envoy:contrib-dev \
envoy -c /etc/envoy/envoy.yaml

test:
curl -s -I 'http://localhost:10000/'
curl -s -I 'http://localhost:10000/' -H 'Authorization: Basic dW5rbm93bjpkb2dvb2Q=' # generated by `echo -n "unknown:dogood" | base64`
curl -s -I 'http://localhost:10000/' -H 'Authorization: Basic aGFja2Vyczp1bmtub3du' # generated by `echo -n "hackers:unknown" | base64`
curl -s -I 'http://localhost:10000/' -H 'Authorization: Basic aGFja2Vyczpkb2dvb2Q=' # generated by `echo -n "hackers:dogood" | base64`
216 changes: 213 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,221 @@
envoy-go-ldap-auth
==================

This is a simple LDAP auth filter for envoy written in go.
This is a simple LDAP auth filter for envoy written in go. Only requests that pass the LDAP server's authentication will be proxied to the upstream service.

During this process, we can optimize the system by implementing user information caching with a duration defined by `config.cache_ttl`. This approach will help reduce the frequency of LDAP server access. If it is set to 0, caching is disabled.

Status
======
In terms of caching, we utilize [bigcache](https://github.com/allegro/bigcache), which demonstrates exceptional performance in the evicting cache domain.
Makonike marked this conversation as resolved.
Show resolved Hide resolved

## Status

This is under active development and is not ready for production use.

## Usage

The client set credentials in `Authorization` header in the following format:

```Plaintext
credentials := Basic base64(username:password)
```

An example of the `Authorization` header is as follows (`aGFja2Vyczpkb2dvb2Q=`, which is the base64-encoded value of `hackers:dogood`):

```Plaintext
Authorization: Basic aGFja2Vyczpkb2dvb2Q=
```

Configure your envoy.yaml, include required fields: host, port, base_dn and attribute.

```yaml
http_filters:
- name: envoy.filters.http.golang
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: example
library_path: /etc/envoy/libgolang.so
plugin_name: envoy-go-ldap-auth
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
# required
host: localhost
port: 389
base_dn: dc=example,dc=com
attribute: cn
# optional
# be used in search mode
bind_dn:
bind_password:
# if the filter is set, the filter application will run in search mode.
filter:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better add an example config as demo, in comment could be good enough.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

cache_ttl: 0
Makonike marked this conversation as resolved.
Show resolved Hide resolved
timeout: 60
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add comment: unit is second.

```

Then, you can start your filter.

```bash
make build
make run
```

## Test

This test case is based on glauth and can be utilized to evaluate your filter.

Firstly, download [glauth](https://github.com/glauth/glauth/releases), and change its [sample config file](https://github.com/glauth/glauth/blob/master/v2/sample-simple.cfg).

sample.yaml

```yaml
[ldap]
enabled = true
# run on a non privileged port
listen = "192.168.64.1:3893" # 192.168.64.1 is your local network IP. Please synchronize it with envoy.yaml.
```

envoy.yaml, and you can find this example file in `example` directory

```yaml
http_filters:
- name: envoy.filters.http.golang
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.golang.v3alpha.Config
library_id: example
library_path: /etc/envoy/libgolang.so
plugin_name: envoy-go-ldap-auth
plugin_config:
"@type": type.googleapis.com/xds.type.v3.TypedStruct
value:
# required
host: 192.168.64.1
port: 3893
base_dn: dc=glauth,dc=com
attribute: cn
# optional
# be used in search mode
bind_dn: cn=serviceuser,ou=svcaccts,dc=glauth,dc=com
bind_password: mysecret
# if the filter is set, the filter application will run in search mode.
filter: (cn=%s)
cache_ttl: 0
timeout: 60
```

Then, start glauth.

```bash
./glauth -c sample.yaml
```

Start and test filter.

```bash
make build
make test-run
make test
```

The following are the output results.

```bash
$ make test
curl -s -I 'http://localhost:10000/'
HTTP/1.1 401 Unauthorized
content-length: 16
content-type: text/plain
date: Sun, 28 May 2023 16:56:18 GMT
server: envoy

curl -s -I 'http://localhost:10000/' -H 'Authorization: Basic dW5rbm93bjpkb2dvb2Q=' # generated by `echo -n "unknown:dogood" | base64`
HTTP/1.1 401 Unauthorized
content-length: 28
content-type: text/plain
date: Sun, 28 May 2023 16:56:18 GMT
server: envoy

curl -s -I 'http://localhost:10000/' -H 'Authorization: Basic aGFja2Vyczp1bmtub3du' # generated by `echo -n "hackers:unknown" | base64`
HTTP/1.1 401 Unauthorized
content-length: 28
content-type: text/plain
date: Sun, 28 May 2023 16:56:18 GMT
server: envoy

curl -s -I 'http://localhost:10000/' -H 'Authorization: Basic aGFja2Vyczpkb2dvb2Q=' # generated by `echo -n "hackers:dogood" | base64`
HTTP/1.1 200 OK
date: Sun, 28 May 2023 16:56:20 GMT
content-type: text/html; charset=utf-8
content-length: 12725
permissions-policy: interest-cohort=()
last-modified: Sat, 06 May 2023 08:09:59 GMT
access-control-allow-origin: *
strict-transport-security: max-age=31556952
etag: "64560b57-31b5"
expires: Sun, 28 May 2023 17:04:05 GMT
cache-control: max-age=600
x-proxy-cache: MISS
x-github-request-id: FD8A:64E8:E68E8:F6229:6473872C
accept-ranges: bytes
via: 1.1 varnish
age: 136
x-served-by: cache-hkg17920-HKG
x-cache: HIT
x-cache-hits: 1
x-timer: S1685292981.889966,VS0,VE30
vary: Accept-Encoding
x-fastly-request-id: 5c0885547b1360efd368516eb77213e7a43586ba
server: envoy
x-envoy-upstream-service-time: 1974
```

## Bind Mode and Search Mode

If no filter is specified in its configuration, the middleware runs in the default bind mode, meaning it tries to make a simple bind request to the LDAP server with the credentials provided in the request headers. If the bind succeeds, the middleware forwards the request, otherwise it returns a `401 Unauthorized` status code.

If a filter query is specified in the middleware configuration, and the Authentication Source referenced has a `bindDN` and a `bindPassword`, then the middleware runs in search mode. In this mode, a search query with the given filter is issued to the LDAP server before trying to bind. If result of this search returns only 1 record, it tries to issue a bind request with this record, otherwise it aborts a `401 Unauthorized` status code.

## Configurations

### Required

- host, string, default "localhost", required

Host on which the LDAP server is running.

- port, number, default 389, required

TCP port where the LDAP server is listening. 389 is the default port for LDAP.

- base_dn, string, "dc=example,dc=com", required

The `baseDN` option should be set to the base domain name that should be used for bind and search queries.

- attribute, string, default "cn", required

Attribute to be used to search the user; e.g., “cn”.

### Optional

- filter, string, default ""

If not empty, the middleware will run in search mode, filtering search results with the given query.

Filter queries can use the `%s` placeholder that is replaced by the username provided in the `Authorization` header of the request. For example: `(&(objectClass=inetOrgPerson)(gidNumber=500)(uid=%s))`, `(cn=%s)`.

- bind_dn, string, default ""

The domain name to bind to in order to authenticate to the LDAP server when running on search mode. Leaving this empty with search mode means binds are anonymous, which is rarely expected behavior. It is not used when running in bind_mode.

- bind_password, string, default ""

The password corresponding to the `bindDN` specified when running in search mode, used in order to authenticate to the LDAP server.

- cache_ttl, number, default 0
doujiang24 marked this conversation as resolved.
Show resolved Hide resolved

Cache expiry time in seconds. If it is set to 0, caching is disabled.

- timeout, number, default 60

An optional timeout in seconds when waiting for connection with LDAP server.

101 changes: 101 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package main

import (
xds "github.com/cncf/xds/go/xds/type/v3"
"github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/api"
"github.com/envoyproxy/envoy/contrib/golang/filters/http/source/go/pkg/http"
"google.golang.org/protobuf/types/known/anypb"
)

func init() {
http.RegisterHttpFilterConfigFactory("envoy-go-ldap-auth", configFactory)
http.RegisterHttpFilterConfigParser(&parser{})
}

type config struct {
host string
port uint64
baseDN string
attribute string
bindDN string
password string
filter string
timeout int32
}

type parser struct {
}

func (p *parser) Parse(any *anypb.Any) (interface{}, error) {
configStruct := &xds.TypedStruct{}
if err := any.UnmarshalTo(configStruct); err != nil {
return nil, err
}

v := configStruct.Value
conf := &config{}
m := v.AsMap()
if host, ok := m["host"].(string); ok {
conf.host = host
}
if port, ok := m["port"].(float64); ok {
conf.port = uint64(port)
}
if baseDN, ok := m["base_dn"].(string); ok {
conf.baseDN = baseDN
}
if attribute, ok := m["attribute"].(string); ok {
conf.attribute = attribute
}
if bindDN, ok := m["bind_dn"].(string); ok {
conf.bindDN = bindDN
}
if password, ok := m["bind_password"].(string); ok {
conf.password = password
}
if cFilter, ok := m["filter"].(string); ok {
conf.filter = cFilter
}

if timeout, ok := m["timeout"].(float64); ok {
conf.timeout = int32(timeout)
}
if conf.timeout == 0 {
conf.timeout = 60
}
return conf, nil
}

func (p *parser) Merge(parent interface{}, child interface{}) interface{} {
panic("TODO")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this TODO need to be addressed.

}

func configFactory(c interface{}) api.StreamFilterFactory {
conf, ok := c.(*config)
if !ok {
panic("unexpected config type, should not happen")
}
return func(callbacks api.FilterCallbackHandler) api.StreamFilter {
return &filter{
callbacks: callbacks,
config: conf,
}
}
}
Loading