Skip to content

Commit 0cea1ad

Browse files
operatrCopilot
andauthored
implement adding users to ad, add zap logging framework, and unit tests (#1)
* implement adding users to ad, add zap logging framework * feat(add-user): AD unicodePwd flow; allow create without password; trim comments * chore(lint): check r.Body.Close and logger.Sync; fix imports ordering * refactor: split ldaps client and add OU support * Refactor handlers and add server tests * Document handlers and lift docs * Badge GPL license * fix golintci errors * Add Go test workflow * Simplify UserClient and remove UserOU * Wire logger into LDAPS client * Drop request body close * Fix DC parsing * Preserve password whitespace * Inline middleware constructors * Adjust tests and workflow * Clarify username validation * Document unicodePwd password handling * Ignore VS Code settings * Add TOC to dev doc * Add CodeQL workflow * Update CodeQL workflow * Cache username regex * Log response status * Add custom schema attributes * Limit create member payload * Escape additional DN chars * Add givenName attribute * add test coverage calculation * fix workflow version * Fix coverage artifact upload in go-coverage workflow (#5) * Initial plan * Initial plan for fixing coverage upload Co-authored-by: operatr <96798445+operatr@users.noreply.github.com> * Add upload-artifact step to fix coverage report upload failure Co-authored-by: operatr <96798445+operatr@users.noreply.github.com> * Delete coverage.out --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: operatr <96798445+operatr@users.noreply.github.com> * . * . * delete code coverage analyzer for now * add unit tests for unicode_pwd --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent 9334fef commit 0cea1ad

22 files changed

Lines changed: 1156 additions & 308 deletions
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
branches: [ main, implement_add_user ]
6+
pull_request:
7+
branches: [ main, implement_add_user ]
8+
9+
jobs:
10+
analyze:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
actions: read
14+
contents: read
15+
security-events: write
16+
pages: none
17+
steps:
18+
- name: Check out repository
19+
uses: actions/checkout@v4
20+
21+
- name: Initialize CodeQL
22+
uses: github/codeql-action/init@v4
23+
with:
24+
languages: go
25+
26+
- name: Autobuild
27+
uses: github/codeql-action/autobuild@v4
28+
29+
- name: Run CodeQL analysis
30+
uses: github/codeql-action/analyze@v4
31+
with:
32+
category: "Go"

.github/workflows/go-tests.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Go Tests
2+
3+
on:
4+
push:
5+
branches: [ main, implement_add_user ]
6+
pull_request:
7+
branches: [ main, implement_add_user ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Check out code
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Go
18+
uses: actions/setup-go@v5
19+
with:
20+
go-version: 1.21
21+
22+
- name: Cache Go modules
23+
uses: actions/cache@v3
24+
with:
25+
path: |
26+
~/.cache/go-build
27+
${{ env.GOMODCACHE }}
28+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
29+
restore-keys: |
30+
${{ runner.os }}-go-
31+
env:
32+
GOMODCACHE: ${{ github.workspace }}/.cache/pkg/mod
33+
34+
- name: Run handler tests
35+
run: go test ./tests/server
36+
37+
- name: Run full test suite
38+
run: go test ./...

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
*.pem
22
.DS_Store
3+
result.txt
4+
.vscode/settings.json
5+
coverage.out

Makefile

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Makefile for common development tasks
2+
3+
.PHONY: fmt lint test ci
4+
5+
# Format imports and code. Requires goimports to be installed.
6+
fmt:
7+
gofmt -w .
8+
goimports -w -local github.com/lugatuic/goberus .
9+
10+
# Run linters using golangci-lint (expects .golangci.yml/.golangci.yaml configured).
11+
lint:
12+
golangci-lint run
13+
14+
# Run unit tests
15+
test:
16+
go test ./...
17+
18+
# CI convenience target: format, lint, test
19+
ci: fmt lint test

README.md

Lines changed: 15 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,117 +1,24 @@
11
# Goberus
22

3-
Goberus is a minimal LDAPS middleware implementing a get_member_info endpoint for Active Directory / LDAP.
4-
This repository contains a small Go service that connects to an LDAPS server, searches for a user (by userPrincipalName or sAMAccountName), and returns a JSON representation of select attributes.
3+
![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)
54

6-
This repo is intentionally minimal so you can validate functionality before we extend it further (password operations, API auth, pooling, tests, etc.).
5+
A minimal LDAP-backed service that exposes member lookup and provisioning workflows via `/v1/member`.
76

8-
Status
9-
- Initial implementation: GET /v1/member?username=<username>
10-
- LDAPS client dials and binds per request (simple, robust for testing)
11-
- TLS verification is configurable (recommended to provide a CA cert)
7+
## Status
8+
- [x] `GET /v1/member?username=<value>` — resolves a user by UPN or sAMAccountName and returns normalized attributes via `server.UserClient` backed by `ldaps.Client` in production and fakes in tests.
9+
- [x] `POST /v1/member` — sanitizes the JSON payload (trim + lowercase for `username`/`OrganizationalUnit`) with `handlers.SanitizeUser` before invoking `ldaps.Client.AddUser`.
10+
- [ ] `DELETE /v1/member` — TODO: expose member removal once LDAP delete semantics and authorization are finalized.
11+
- [ ] `PATCH /v1/member` — TODO: introduce attribute updates once LDAP modify flows are defined.
12+
- `tests/server/handlers_test.go` covers the handler flows (including an integration-style `httptest.NewServer` canary), and `handlers/users_test.go` focuses on `SanitizeUser`.
1213

13-
Quick start — run locally
14-
1. Prerequisites
15-
- Go 1.21+
16-
- Network access to your Active Directory on LDAPS (TCP 636)
14+
## Development & testing
15+
See [docs/dev-setup.md](docs/dev-setup.md) for the quick-start instructions, environment variables, Docker guidance, troubleshooting tips, and the testing commands (`go test ./tests/server -run TestHandleGetMember`, `go test ./tests/server -run TestHandleCreateMember`, `go test ./tests/server -run TestSanitizeUserIntegration`, `go test ./...`).
1716

18-
2. Set environment variables (example)
19-
```bash
20-
export BIND_ADDR="8080"
21-
export LDAP_ADDR="ad.example.local:636" # host:port for LDAPS
22-
export LDAP_BASE_DN="DC=example,DC=local"
23-
export LDAP_BIND_DN="CN=svc-goberus,OU=svc,DC=example,DC=local"
24-
export LDAP_BIND_PASSWORD="supersecret"
25-
export LDAP_SKIP_VERIFY="false" # set true only for dev testing
26-
# Or better: provide CA cert that signed the AD server cert:
27-
# export LDAP_CA_CERT="/path/to/ca.pem"
28-
```
29-
30-
3. Build and run
31-
```bash
32-
go mod tidy
33-
go build -o goberus ./...
34-
./goberus
35-
```
36-
37-
4. Query the service
38-
```bash
39-
curl 'http://localhost:8080/v1/member?username=jdoe' | jq .
40-
# or with UPN:
41-
curl 'http://localhost:8080/v1/member?username=jdoe@example.local' | jq .
42-
```
43-
44-
Docker — build and run
45-
A multi-stage Dockerfile is provided to build a small runtime image.
46-
47-
Build:
48-
```bash
49-
docker build -t lugatuic/goberus:latest .
50-
```
51-
52-
Run (example):
53-
```bash
54-
docker run --rm -p 8080:8080 \
55-
-e BIND_ADDR=":8080" \
56-
-e LDAP_ADDR="ad.example.local:636" \
57-
-e LDAP_BASE_DN="DC=example,DC=local" \
58-
-e LDAP_BIND_DN="CN=svc-goberus,OU=svc,DC=example,DC=local" \
59-
-e LDAP_BIND_PASSWORD="supersecret" \
60-
-e LDAP_SKIP_VERIFY="false" \
61-
lugatuic/goberus:latest
62-
```
63-
64-
If you need to provide a CA certificate file to verify the LDAPS server certificate, mount it into the container and set `LDAP_CA_CERT` to the path inside the container:
65-
```bash
66-
docker run --rm -p 8080:8080 \
67-
-v /local/path/ca.pem:/etc/ssl/certs/goberus-ca.pem:ro \
68-
-e LDAP_CA_CERT="/etc/ssl/certs/goberus-ca.pem" \
69-
... lugatuic/goberus:latest
70-
```
71-
72-
CI tip — bake CA into the image from a secret (GitHub Actions example)
73-
If you store your CA PEM as a repository secret (recommended for private/internal CAs), write it to `ad_chain.pem` in the workflow and build the image. The `Dockerfile` will copy `ad_chain.pem` into `/etc/ssl/certs/goberus-ca.pem` and set `LDAP_CA_CERT`.
74-
75-
Example GitHub Actions step (assumes secret name GOBERUS_CA_PEM):
76-
77-
```yaml
78-
- name: Build image (write CA from secret)
79-
run: |
80-
echo "$GOBERUS_CA_PEM" > ad_chain.pem
81-
docker build --build-arg TARGETARCH=amd64 --build-arg TARGETOS=linux -t lugatuic/goberus:latest .
82-
env:
83-
GOBERUS_CA_PEM: ${{ secrets.GOBERUS_CA_PEM }}
84-
```
85-
86-
Notes:
87-
- Do NOT commit `ad_chain.pem` to the repository — it's added to `.gitignore` by default.
88-
- The workflow writes the secret to a file only during the run and uses it to create the image. This keeps the private CA material out of the repo while allowing the image to include the CA at build time.
89-
- If you prefer not to bake the CA into the image, mount it at runtime instead (see example above).
90-
91-
Environment variables reference
92-
- BIND_ADDR — HTTP listen address (default `:8080`)
93-
- LDAP_ADDR — LDAPS address, host:port (required)
94-
- LDAP_BASE_DN — base DN for searches (required)
95-
- LDAP_BIND_DN — optional service DN used for searches/modify (recommended)
96-
- LDAP_BIND_PASSWORD — password for LDAP_BIND_DN
97-
- LDAP_SKIP_VERIFY — "true" to skip TLS verification (development only)
98-
- LDAP_CA_CERT — path to CA PEM file used to verify the LDAPS server cert
99-
100-
Behavior & notes
101-
- Authentication: the service currently implements bind-as-user for authentication in the original design; the only exposed endpoint initially is a read/search endpoint (`/v1/member`). We will add API auth (JWT/API keys) later.
102-
- Active Directory specifics: setting/changing passwords in AD requires LDAPS and AD's unicodePwd behavior (UTF-16LE quoted string). That is not implemented yet.
103-
- TLS: for production, do not use `LDAP_SKIP_VERIFY=true`. Provide a CA via `LDAP_CA_CERT` or ensure the directory's cert chains to a trusted root in the container.
104-
105-
Troubleshooting
106-
- "x509: certificate signed by unknown authority": provide LDAP_CA_CERT or ensure the CA is installed/trusted.
107-
- "Bind failed": ensure LDAP_BIND_DN is a full DN and the password is correct; try a manual ldapsearch to validate credentials.
108-
- No entries found: check LDAP_BASE_DN is correct and try searching by different username formats (sAMAccountName and UPN).
109-
110-
Next steps (after you verify this works)
17+
## Next steps
11118
- Add API authentication and rate limiting
11219
- Implement connection pooling/reconnect semantics
113-
- Implement AD-safe password set/change (unicodePwd handling over LDAPS)
114-
- Add unit tests + integration tests (Docker-compose with Samba AD)
20+
- Handle LDAPS password changes via `unicodePwd`
21+
- Expand unit/integration coverage (e.g., Docker-compose with Samba AD)
11522

116-
License
117-
- Add your preferred license; none is included by default.
23+
## License
24+
Goberus is open-source software distributed under the terms of the [GNU General Public License v3](LICENSE). See `LICENSE` for the full text and warranty disclaimer.

docs/dev-setup.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Development setup
2+
3+
## Table of Contents
4+
- [Quick start — run locally](#quick-start--run-locally)
5+
- [Docker — build and run](#docker--build-and-run)
6+
- [CI tip](#ci-tip)
7+
- [Environment variables reference](#environment-variables-reference)
8+
- [Behavior & notes](#behavior--notes)
9+
- [Troubleshooting](#troubleshooting)
10+
- [Testing](#testing)
11+
12+
This document walks through the common commands for running and building Goberus locally.
13+
14+
## Quick start — run locally
15+
1. Prerequisites
16+
- Go 1.21+
17+
- Network access to your Active Directory on LDAPS (TCP 636)
18+
19+
2. Set environment variables (example)
20+
```bash
21+
export BIND_ADDR="8080"
22+
export LDAP_ADDR="ad.example.local:636" # host:port for LDAPS
23+
export LDAP_BASE_DN="DC=example,DC=local"
24+
export LDAP_BIND_DN="CN=svc-goberus,OU=svc,DC=example,DC=local"
25+
export LDAP_BIND_PASSWORD="supersecret"
26+
export LDAP_SKIP_VERIFY="false" # set true only for dev testing
27+
# Or better: provide CA cert that signed the AD server cert:
28+
# export LDAP_CA_CERT="/path/to/ca.pem"
29+
```
30+
31+
3. Build and run
32+
```bash
33+
go mod tidy
34+
go build -o goberus ./...
35+
./goberus
36+
```
37+
38+
4. Query the service
39+
```bash
40+
curl 'http://localhost:8080/v1/member?username=jdoe' | jq .
41+
# or with UPN:
42+
curl 'http://localhost:8080/v1/member?username=jdoe@example.local' | jq .
43+
44+
curl --header "Content-Type: application/json" \
45+
--request POST \
46+
--data '{"username":"testuser","password":"S3cureP@ss"}' \
47+
http://localhost:8080/v1/member | jq .
48+
```
49+
50+
## Docker — build and run
51+
The Dockerfile uses a multi-stage build to produce a minimal runtime image.
52+
53+
Build:
54+
```bash
55+
docker build -t lugatuic/goberus:latest .
56+
```
57+
58+
Run (example):
59+
```bash
60+
docker run --rm -p 8080:8080 \
61+
-e BIND_ADDR=":8080" \
62+
-e LDAP_ADDR="ad.example.local:636" \
63+
-e LDAP_BASE_DN="DC=example,DC=local" \
64+
-e LDAP_BIND_DN="CN=svc-goberus,OU=svc,DC=example,DC=local" \
65+
-e LDAP_BIND_PASSWORD="supersecret" \
66+
-e LDAP_SKIP_VERIFY="false" \
67+
lugatuic/goberus:latest
68+
```
69+
70+
To provide a CA certificate file inside the container, mount it and set `LDAP_CA_CERT`:
71+
```bash
72+
docker run --rm -p 8080:8080 \
73+
-v /local/path/ca.pem:/etc/ssl/certs/goberus-ca.pem:ro \
74+
-e LDAP_CA_CERT="/etc/ssl/certs/goberus-ca.pem" \
75+
... lugatuic/goberus:latest
76+
```
77+
78+
## CI tip
79+
If you store your CA PEM in a GitHub Actions secret (recommended for private/internal CAs), write it to `ad_chain.pem` during the workflow and build the image. The Dockerfile copies that file to `/etc/ssl/certs/goberus-ca.pem` and sets `LDAP_CA_CERT`.
80+
81+
Example step:
82+
```yaml
83+
- name: Build image (write CA from secret)
84+
run: |
85+
echo "$GOBERUS_CA_PEM" > ad_chain.pem
86+
docker build --build-arg TARGETARCH=amd64 --build-arg TARGETOS=linux -t lugatuic/goberus:latest .
87+
env:
88+
GOBERUS_CA_PEM: ${{ secrets.GOBERUS_CA_PEM }}
89+
```
90+
91+
> **Notes**
92+
> - Do NOT commit `ad_chain.pem` to the repository — it is ignored by `.gitignore`.
93+
> - The workflow writes the secret only during the run and uses it to build the image, keeping sensitive data out of the repo.
94+
> - If you prefer not to bake the CA into the image, mount it at runtime instead (see previous section).
95+
96+
## Environment variables reference
97+
- `BIND_ADDR` — HTTP listen address (default `:8080`)
98+
- `LDAP_ADDR` — LDAPS address, host:port (required)
99+
- `LDAP_BASE_DN` — base DN for searches (required)
100+
- `LDAP_BIND_DN` — optional service DN used for searches/modify (recommended)
101+
- `LDAP_BIND_PASSWORD` — password for `LDAP_BIND_DN`
102+
- `LDAP_SKIP_VERIFY` — set to `true` to skip TLS verification (development only)
103+
- `LDAP_CA_CERT` — path to a CA PEM file used to verify the LDAPS server cert
104+
105+
## Behavior & notes
106+
- Authentication: the current implementation prefers bind-as-user for authentication; only the read/search endpoint (`/v1/member`) and the POST `/v1/member` user creation endpoint are exposed.
107+
- Active Directory password operations run over LDAPS using AD's `unicodePwd` behavior when creating users (`ldaps.AddUser` now calls `setUnicodePwd` and `enableAccount`).
108+
- TLS: do not use `LDAP_SKIP_VERIFY=true` in production. Provide a CA via `LDAP_CA_CERT` or trust a CA that already exists in the container.
109+
110+
## Troubleshooting
111+
- `x509: certificate signed by unknown authority`: provide `LDAP_CA_CERT` or ensure the CA is trusted.
112+
- `Bind failed`: ensure `LDAP_BIND_DN` is a full DN and the password is correct; validate with `ldapsearch` if necessary.
113+
- No entries found: verify `LDAP_BASE_DN` and try searching by different username formats (sAMAccountName, UPN).
114+
115+
## Testing
116+
- `go test ./tests/server -run TestHandleGetMember`
117+
- `go test ./tests/server -run TestHandleCreateMember`
118+
- `go test ./tests/server -run TestSanitizeUserIntegration`
119+
- `go test ./...`

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.21
44

55
require (
66
github.com/go-ldap/ldap/v3 v3.4.0
7+
github.com/matryer/is v1.2.0
78
go.uber.org/zap v1.27.1
89
)
910

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,29 @@
11
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
22
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
3+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
46
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
57
github.com/go-ldap/ldap/v3 v3.4.0 h1:wCttA0dcqAOygfOabqYhQPXKGG9ws8az3FBM8+GAhDs=
68
github.com/go-ldap/ldap/v3 v3.4.0/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
9+
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
10+
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
11+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13+
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
14+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
15+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
16+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
17+
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
18+
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
19+
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
20+
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
721
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
822
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
923
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
1024
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
1125
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
1226
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
1327
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
28+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
29+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)