Release v0.1.0

- Initial release of LDAP Docker development tool
- Full .env configuration support with comprehensive documentation
- Pre-configured test users and SSL/TLS support
- Consolidated documentation in README
This commit is contained in:
2025-10-20 12:32:48 -07:00
parent 857c71484a
commit 7db3584ad3
16 changed files with 552 additions and 2618 deletions

View File

@@ -5,90 +5,135 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.1.0] - 2025-01-XX
## [0.1.0] - 2025-10-20
### Added
- Initial release of LDAP Docker development tool
- OpenLDAP 1.5.0 container with SSL/TLS support
- phpLDAPadmin web interface for easy administration
- Pre-configured test users and groups for testing.local domain
- SSL certificate generation script using Python cryptography
- Comprehensive CLI tool for managing LDAP server (`ldap-docker` command)
- Makefile with convenient shortcuts for common operations
- Interactive quickstart script (`quickstart.sh`) for guided setup
- Example Python authentication script demonstrating LDAP integration
- Example Python authentication script demonstrating LDAP integration with environment variable support
- Support for custom dev-ca certificates
- Persistent Docker volumes for data and configuration
- Test suite for certificate generation
- Comprehensive environment variable reference table in Configuration section with usage cross-references
- `{.env:VARIABLE_NAME}` syntax throughout documentation to indicate configurable values
- Explanatory notes about `.env` configuration flexibility at key sections
- Comprehensive documentation:
- README.md - Full project documentation
- GETTING_STARTED.md - Beginner-friendly guide
- QUICKREF.md - Quick command reference
- README.md - Full project documentation with integrated configuration guide
- certs/README.md - Certificate management guide
- examples/README.md - Integration patterns and examples
- examples/README.md - Integration patterns and code examples
### Changed
- Renamed Makefile targets from `test-*` to `verify-*` for integration checks that require a running container
- `test-connection``verify-connection`
- `test-auth``verify-auth`
- `test-users``verify-users`
- `test-ssl``verify-ssl`
- `test-all``verify-all`
- Added separate `make test` and `make test-cov` targets for running unit tests with pytest
- Improved naming clarity: "verify" commands check running containers, "test" commands run unit tests
### Test Data
- 4 pre-configured test users (admin, jdoe, jsmith, testuser)
- 3 test groups (admins, developers, users)
- All test users use password: `password123`
- Admin credentials: `cn=admin,dc=testing,dc=local` / `admin_password`
### Infrastructure
- Docker Compose configuration for easy deployment
- UV package manager integration for Python dependencies
- `.env` file support for configuration
- Cross-platform support (MacOS, Linux, Windows)
- Rancher Desktop and Docker Desktop compatibility
### Configuration
- Environment variable support for all configurable values
- `.env.example` file with comprehensive documentation
- Configurable ports (LDAP, LDAPS, phpLDAPadmin)
- Configurable domain and organization settings
- Configurable SSL/TLS certificate filenames
- Configurable admin credentials
### Documentation
- README with clear Quick Start, Configuration, and Troubleshooting sections
- All code examples (Python, bash, ldapsearch) reference `.env` variables
- examples/README.md with `.env` variable references in all code samples
- certs/README.md with `.env` variables for hostnames, ports, and certificate filenames
- Admin Credentials, Testing Authentication, and Certificate Requirements sections use `.env` syntax
- Default Test Users table shows dynamic email addresses based on `LDAP_DOMAIN`
- Development section LDIF examples reference `.env` variables
### Fixed
- Updated `pyproject.toml` to use `dependency-groups.dev` instead of deprecated `tool.uv.dev-dependencies`
- Added `tool.hatch.build.targets.wheel.packages` configuration to fix build errors
- Removed obsolete `version` field from `docker-compose.yml` (Docker Compose v2+ compatibility)
- Fixed LDAP user password hashes to use proper SSHA format generated by `slappasswd`
- Fixed attribute type conversion in example scripts for uidNumber and gidNumber
- Fixed `scripts/__init__.py` import error that prevented pytest from running
- Fixed timezone-aware datetime comparisons in certificate generation tests (updated to use `not_valid_before_utc` and `not_valid_after_utc`)
### Technical Details
- Base DN: `dc=testing,dc=local`
- LDAP Port: 389 (standard)
- LDAPS Port: 636 (SSL/TLS)
- Web Admin Port: 8080
- Python 3.9+ required
- Base DN: `dc=testing,dc=local` (configurable via `LDAP_BASE_DN`)
- LDAP Port: 389 (configurable via `LDAP_PORT`)
- LDAPS Port: 636 (configurable via `LDAPS_PORT`)
- Web Admin Port: 8080 (configurable via `PHPLDAPADMIN_PORT`)
- Python 3.9+ required (optional, for certificate generation and examples)
- Docker/Rancher Desktop required
## [Unreleased]
### Planned Features
- Additional integration examples (Node.js, Go, Ruby, etc.)
- Health check endpoints
- Automated backup scripts
- Docker image with pre-built configuration
- Kubernetes/Helm deployment examples
- LDAP replication setup guide
- Performance tuning guide
- Security hardening options
---
## Release Notes
### Version 0.1.0
This is the initial release providing a complete LDAP development environment suitable for:
- Testing LDAP authentication in applications
- Development and integration testing
- Learning LDAP concepts
- Prototyping LDAP-based systems
**Key Features:**
- Quick setup with `docker-compose up -d`
- Fully configurable via `.env` file
- Pre-populated with test users and groups
- SSL/TLS support with custom certificate capability
- Web-based administration interface
- Comprehensive documentation with clear examples
**Important Security Notes:**
- This tool is for **DEVELOPMENT USE ONLY**
- Default passwords are well-known and insecure
- Self-signed certificates are not suitable for production
- Never use this with real user data or in production environments
### Upgrade Instructions
Not applicable for initial release.
### Breaking Changes
Not applicable for initial release.
---
For support, issues, or feature requests, please refer to the project documentation or open an issue on the project repository.
For support, issues, or feature requests, please refer to the project documentation or open an issue on the project repository.

View File

@@ -1,288 +0,0 @@
# Getting Started with LDAP Docker
Welcome! This guide will help you get your LDAP development server up and running in just a few minutes.
## What is This?
LDAP Docker is a ready-to-use OpenLDAP server for development and testing. It comes pre-configured with:
- ✅ SSL/TLS support (LDAPS)
- ✅ Test users and groups
- ✅ Web-based admin interface
- ✅ Easy certificate management
- ✅ Simple Python CLI tools
Perfect for testing applications that need LDAP authentication without setting up a complex infrastructure.
## Prerequisites
You'll need these installed on your Mac:
1. **Docker or Rancher Desktop** (for running containers)
- Rancher Desktop: https://rancherdesktop.io/ (recommended for Mac)
- Or Docker Desktop: https://www.docker.com/products/docker-desktop
2. **Python 3.9+** (usually pre-installed on Mac)
```bash
python3 --version
```
3. **UV Package Manager** (optional but recommended)
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
## Quick Start (5 Minutes)
### Option 1: Automated Setup (Easiest)
Run the quick start script that will guide you through everything:
```bash
./quickstart.sh
```
This interactive script will:
1. Check all prerequisites
2. Install UV (if you want)
3. Generate SSL certificates
4. Start the LDAP server
5. Verify everything works
### Option 2: Manual Setup (Step by Step)
If you prefer to understand each step:
```bash
# 1. Install Python dependencies
make install
# 2. Generate SSL certificates
make certs-generate
# 3. Start the LDAP server
make start
# 4. Test the connection
make test-connection
```
That's it! Your LDAP server is now running.
## Using Your Own Dev-CA Certificates
Since you mentioned you maintain a local dev-ca, here's how to use your own certificates:
```bash
# Instead of 'make certs-generate', copy your certificates:
cp /path/to/your/dev-ca/ca-cert.pem certs/ca.crt
cp /path/to/your/dev-ca/ldap-server.crt certs/server.crt
cp /path/to/your/dev-ca/ldap-server.key certs/server.key
# Set proper permissions
chmod 644 certs/ca.crt certs/server.crt
chmod 600 certs/server.key
# Then start the server
make start
```
**Important:** Your server certificate should be issued for hostname `ldap.testing.local` or include it as a Subject Alternative Name (SAN).
## Accessing Your LDAP Server
Once running, you can access:
| Service | URL | Purpose |
|---------|-----|---------|
| **LDAP** | `ldap://localhost:389` | Standard LDAP (unencrypted) |
| **LDAPS** | `ldaps://localhost:636` | Secure LDAP with SSL/TLS |
| **Admin UI** | `http://localhost:8080` | Web interface (phpLDAPadmin) |
### Login Credentials
- **Admin DN:** `cn=admin,dc=testing,dc=local`
- **Password:** `admin_password`
### Test Users (All use password: `password123`)
- `jdoe` - John Doe (jdoe@testing.local)
- `jsmith` - Jane Smith (jsmith@testing.local)
- `testuser` - Test User (testuser@testing.local)
- `admin` - Admin User (admin@testing.local)
## Verify Everything Works
### Test 1: List all users
```bash
make test-users
```
You should see output like:
```
Found 4 user(s):
- Admin User: admin (admin@testing.local)
- John Doe: jdoe (jdoe@testing.local)
- Jane Smith: jsmith (jsmith@testing.local)
- Test User: testuser (testuser@testing.local)
```
### Test 2: Try the web interface
Open http://localhost:8080 in your browser and login with:
- Login DN: `cn=admin,dc=testing,dc=local`
- Password: `admin_password`
### Test 3: Run the example script
```bash
python examples/simple_auth.py
```
This authenticates user `jdoe` and displays their information.
## Common Commands
```bash
# Server management
make start # Start LDAP server
make stop # Stop LDAP server
make restart # Restart LDAP server
make logs # View logs (live)
make status # Check if running
# Testing
make test-users # List all users
make test-auth # Test authentication
make test-all # Run all tests
# Maintenance
make clean # Clean build artifacts
make down # Stop and remove containers
```
## Next Steps
Now that your LDAP server is running, you can:
1. **Integrate with Your Application**
- See `examples/README.md` for code samples
- Point your app to `ldap://localhost:389`
2. **Add Custom Users**
- Edit `ldif/01-users.ldif`
- Run `make down-volumes && make start` to reload
3. **Configure for Your Use Case**
- Edit `docker-compose.yml` for custom settings
- See `.env.example` for environment variables
4. **Learn More**
- `README.md` - Full documentation
- `QUICKREF.md` - Command reference
- `certs/README.md` - Certificate management
- `examples/README.md` - Integration examples
## Troubleshooting
### "Docker is not running"
Start Rancher Desktop from your Applications folder. Look for its icon in the menu bar.
### "Connection refused"
Wait 10-30 seconds after starting - the server needs time to initialize:
```bash
make logs # Watch until you see "slapd starting"
```
### "Certificate errors"
Verify certificates exist:
```bash
ls -la certs/
```
Regenerate if needed:
```bash
make certs-generate --force
```
### "Port already in use"
Check if something is using LDAP ports:
```bash
lsof -i :389
lsof -i :636
```
### Still stuck?
Check the full troubleshooting section in `README.md` or view logs:
```bash
make logs
```
## Docker Basics (If You're New)
Since you mentioned being new to Docker, here are the basics:
- **Container**: A lightweight, isolated environment running your LDAP server
- **Image**: The blueprint for creating containers (we use `osixia/openldap`)
- **Volume**: Persistent storage for LDAP data (survives restarts)
- **docker-compose**: Tool for managing multi-container applications (LDAP + Admin UI)
When you run `make start`, Docker:
1. Downloads the LDAP image (first time only)
2. Creates containers from the image
3. Mounts your certificates and data files
4. Starts the LDAP service
The `Makefile` is just a collection of shortcuts for common Docker commands.
## Building Elsewhere
This project works on:
- ✅ **MacOS** (with Rancher Desktop or Docker Desktop)
- ✅ **Linux** (with Docker installed)
- ✅ **Windows** (with Docker Desktop + WSL2)
The same `docker-compose.yml` works everywhere - that's the beauty of Docker!
To share your setup with a colleague:
```bash
# Just copy the whole project
git clone <your-repo>
cd ldap_docker
make dev-setup
```
## Quick Reference Card
Keep these handy:
```bash
# Start server
make start
# View logs
make logs
# List users
make test-users
# Stop server
make stop
# Help
make help
```
## Support & Documentation
- **Quick commands**: `make help`
- **Full guide**: `README.md`
- **Examples**: `examples/README.md`
- **Command reference**: `QUICKREF.md`
---
**Ready to start?** Run `./quickstart.sh` or `make dev-setup` and you'll be up in 5 minutes! 🚀
If you run into any issues, check the logs with `make logs` or see the Troubleshooting section in `README.md`.

View File

@@ -1,4 +1,4 @@
.PHONY: help install init start stop restart down logs status certs-generate certs-check test-connection test-auth test-users clean clean-all
.PHONY: help install init start stop restart down logs status certs-generate certs-check verify-connection verify-auth verify-users verify-ssl verify-all test test-cov clean clean-all
# Load environment variables from .env file if it exists
ifneq (,$(wildcard ./.env))
@@ -15,7 +15,7 @@ help: ## Show this help message
@echo "Usage: make [target]"
@echo ""
@echo "Available targets:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
install: ## Install Python dependencies with UV
@echo "Installing dependencies with UV..."
@@ -25,7 +25,7 @@ install: ## Install Python dependencies with UV
install-dev: ## Install development dependencies
@echo "Installing development dependencies with UV..."
uv sync --all-extras
uv sync --group dev
@echo "✅ Development dependencies installed"
init: install certs-check ## Initialize the environment (install deps, check certs)
@@ -109,23 +109,33 @@ logs-admin: ## View phpLDAPadmin logs
status: ## Show container status
@docker-compose ps
test-connection: ## Test connection to LDAP server
@echo "Testing LDAP connection..."
verify-connection: ## Verify connection to LDAP server (requires running container)
@echo "Verifying LDAP connection..."
@export LDAP_PORT=$${LDAP_PORT:-389}; uv run python -c "import os; from ldap3 import Server, Connection, ALL; s = Server(f\"ldap://localhost:{os.environ.get('LDAP_PORT', '389')}\", get_info=ALL); c = Connection(s, auto_bind=True); print('✅ Connection successful'); c.unbind()"
test-auth: ## Test authentication with admin user
@echo "Testing LDAP authentication..."
verify-auth: ## Verify authentication with admin user (requires running container)
@echo "Verifying LDAP authentication..."
@export LDAP_PORT=$${LDAP_PORT:-389}; uv run python -c "import os; from ldap3 import Server, Connection; s = Server(f\"ldap://localhost:{os.environ.get('LDAP_PORT', '389')}\"); c = Connection(s, 'cn=admin,dc=testing,dc=local', 'admin_password', auto_bind=True); print('✅ Authentication successful'); c.unbind()"
test-users: ## List all users in LDAP
verify-users: ## List all users in LDAP (requires running container)
@echo "Listing LDAP users..."
@export LDAP_PORT=$${LDAP_PORT:-389}; uv run python -c "import os; from ldap3 import Server, Connection; s = Server(f\"ldap://localhost:{os.environ.get('LDAP_PORT', '389')}\"); c = Connection(s, 'cn=admin,dc=testing,dc=local', 'admin_password', auto_bind=True); c.search('dc=testing,dc=local', '(objectClass=inetOrgPerson)', attributes=['uid', 'cn', 'mail']); [print(f' - {e.cn}: {e.uid} ({e.mail})') for e in c.entries]; c.unbind()"
test-ssl: ## Test SSL/TLS connection
@echo "Testing LDAPS connection..."
verify-ssl: ## Verify SSL/TLS connection (requires running container)
@echo "Verifying LDAPS connection..."
openssl s_client -connect localhost:$${LDAPS_PORT:-636} -CAfile certs/ca.crt </dev/null
test-all: test-connection test-auth test-users ## Run all tests
verify-all: verify-connection verify-auth verify-users ## Run all verification checks (requires running container)
test: ## Run unit tests with pytest
@echo "Running unit tests..."
uv run pytest
test-cov: ## Run unit tests with coverage report
@echo "Running unit tests with coverage..."
uv run pytest --cov=scripts --cov-report=term-missing --cov-report=html
@echo ""
@echo "📊 Coverage report generated in htmlcov/index.html"
shell: ## Open a shell in the LDAP container
docker-compose exec openldap bash
@@ -141,7 +151,7 @@ clean: ## Clean Python build artifacts
find . -type f -name "*.pyo" -delete 2>/dev/null || true
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true
find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true
find . -type f -name ".coverage" -delete 2>/dev/null || true
rm -rf build/ dist/ .eggs/
@@ -158,8 +168,8 @@ dev-setup: install-dev certs-generate start ## Complete development setup
@echo ""
@echo "Next steps:"
@echo " - View logs: make logs"
@echo " - Test connection: make test-connection"
@echo " - List users: make test-users"
@echo " - Verify connection: make verify-connection"
@echo " - List users: make verify-users"
@echo " - Open admin UI: open http://localhost:8080"
quick-start: certs-check start ## Quick start (assumes certs exist)

View File

@@ -1,436 +0,0 @@
# Port Configuration Guide
This guide explains how to configure custom ports for your LDAP Docker environment to avoid conflicts with other services.
## Quick Start
**To change ports, simply edit the `.env` file:**
```bash
# Edit .env file
LDAP_PORT=20389
LDAPS_PORT=20636
PHPLDAPADMIN_PORT=8080
```
Then restart:
```bash
make down && make start
```
That's it! All scripts and tools will automatically use your custom ports.
---
## Why Custom Ports?
You might want to use custom ports if:
- You have another LDAP server running on standard ports (389/636)
- Port 389 requires root/admin privileges
- You're running multiple LDAP environments simultaneously
- Your organization has specific port requirements
## Files Involved
The port configuration system uses these files:
### 1. `.env` - Your Configuration (Edit This!)
```bash
LDAP_PORT=20389 # Your custom LDAP port
LDAPS_PORT=20636 # Your custom LDAPS port
PHPLDAPADMIN_PORT=8080 # Web admin interface port
```
### 2. `docker-compose.yml` - Uses Environment Variables
```yaml
ports:
- "${LDAP_PORT:-389}:389" # Host:Container mapping
- "${LDAPS_PORT:-636}:636"
```
The `${LDAP_PORT:-389}` syntax means:
- Use `$LDAP_PORT` if set in `.env`
- Otherwise use default `389`
### 3. Scripts - Auto-detect Ports
All Python scripts and Makefile commands read from `.env` automatically:
- `scripts/cli.py`
- `scripts/ldapsearch_helper.sh`
- `examples/simple_auth.py`
- `Makefile`
## How to Change Ports
### Method 1: Edit .env File (Recommended)
1. **Edit `.env`:**
```bash
vim .env # or your favorite editor
```
2. **Change the port values:**
```bash
LDAP_PORT=20389
LDAPS_PORT=20636
```
3. **Restart the containers:**
```bash
make down && make start
```
4. **Verify:**
```bash
docker-compose ps
# Should show 0.0.0.0:20389->389/tcp
```
### Method 2: Environment Variables (Temporary)
For one-time use without modifying `.env`:
```bash
LDAP_PORT=30389 LDAPS_PORT=30636 docker-compose up -d
```
### Method 3: Create .env.local (Advanced)
For personal overrides without modifying the main `.env`:
```bash
# Create .env.local (git-ignored)
echo "LDAP_PORT=12389" > .env.local
echo "LDAPS_PORT=12636" >> .env.local
# Docker Compose will merge both files
docker-compose up -d
```
## Testing with Custom Ports
### Automatic - Use Our Tools
All our tools automatically detect your ports from `.env`:
```bash
# These all work automatically with your custom ports
make test-users
make test-connection
uv run python examples/simple_auth.py
```
### Generate ldapsearch Commands
Use our helper script to generate commands with your ports:
```bash
./scripts/ldapsearch_helper.sh
```
Output includes commands like:
```bash
# List all users
ldapsearch -H ldap://localhost:20389 \
-D "cn=admin,dc=testing,dc=local" \
-w admin_password \
-b "ou=people,dc=testing,dc=local" \
"(objectClass=inetOrgPerson)" \
uid cn mail
```
### Manual ldapsearch Commands
Replace `389` with your `LDAP_PORT` and `636` with your `LDAPS_PORT`:
```bash
# Standard LDAP (unencrypted)
ldapsearch -H ldap://localhost:20389 \
-D "cn=admin,dc=testing,dc=local" \
-w admin_password \
-b "dc=testing,dc=local"
# LDAPS (SSL/TLS)
ldapsearch -H ldaps://localhost:20636 \
-D "cn=admin,dc=testing,dc=local" \
-w admin_password \
-b "dc=testing,dc=local"
# Search for specific user
ldapsearch -H ldap://localhost:20389 \
-D "cn=admin,dc=testing,dc=local" \
-w admin_password \
-b "dc=testing,dc=local" \
"(uid=jdoe)"
# Test user authentication
ldapsearch -H ldap://localhost:20389 \
-D "uid=jdoe,ou=people,dc=testing,dc=local" \
-w password123 \
-b "dc=testing,dc=local" \
"(uid=jdoe)"
```
### Using Python
```python
import os
from ldap3 import Server, Connection
# Automatically uses LDAP_PORT from environment
port = os.environ.get('LDAP_PORT', '389')
server = Server(f'ldap://localhost:{port}')
conn = Connection(server,
user='cn=admin,dc=testing,dc=local',
password='admin_password',
auto_bind=True)
conn.search('dc=testing,dc=local', '(objectClass=*)')
print(f"Connected on port {port}")
conn.unbind()
```
## Common Port Configurations
### Development (Unprivileged Ports)
```bash
LDAP_PORT=20389
LDAPS_PORT=20636
PHPLDAPADMIN_PORT=8080
```
### Testing (High Ports)
```bash
LDAP_PORT=30389
LDAPS_PORT=30636
PHPLDAPADMIN_PORT=8090
```
### Multiple Environments
```bash
# Environment 1 (.env)
LDAP_PORT=20389
LDAPS_PORT=20636
# Environment 2 (separate directory or .env.local)
LDAP_PORT=21389
LDAPS_PORT=21636
```
## Troubleshooting
### Port Already in Use
**Error:** `Bind for 0.0.0.0:20389 failed: port is already allocated`
**Check what's using the port:**
```bash
lsof -i :20389
# or
netstat -an | grep 20389
```
**Solutions:**
1. Stop the conflicting service
2. Choose a different port in `.env`
3. Use `make down` to stop this LDAP server first
### Scripts Not Using Custom Ports
**Problem:** Scripts still connect to port 389
**Solution:** Ensure `.env` is loaded:
1. **Check .env exists:**
```bash
ls -la .env
```
2. **Verify ports are set:**
```bash
grep LDAP_PORT .env
```
3. **Reload environment:**
```bash
source .env # For bash scripts
make down && make start # Restart containers
```
### Docker Compose Not Reading .env
**Problem:** Containers still on default ports
**Check:**
```bash
# View what Docker Compose sees
docker-compose config | grep -A2 ports
```
**Solution:**
```bash
# Ensure .env is in project root
pwd # Should be ldap_docker/
ls .env # Should exist
# Force reload
docker-compose down
docker-compose up -d
```
### Can't Connect After Port Change
**Verify containers are running on correct ports:**
```bash
docker-compose ps
# Should show: 0.0.0.0:20389->389/tcp
```
**Test connectivity:**
```bash
# Test if port is listening
nc -zv localhost 20389
# Test LDAP response
ldapsearch -H ldap://localhost:20389 -x -b "" -s base
```
**Check logs:**
```bash
make logs
# Look for: "slapd starting"
```
## Port Reference Table
| Service | Default Port | Docker Internal Port | Configurable Via |
|---------|--------------|---------------------|------------------|
| LDAP | 389 | 389 | `LDAP_PORT` in .env |
| LDAPS | 636 | 636 | `LDAPS_PORT` in .env |
| phpLDAPadmin | 8080 | 80 | `PHPLDAPADMIN_PORT` in .env |
**Note:** The "Docker Internal Port" (right side of mapping) never changes. Only the host port (left side) is configurable.
## Advanced: Port Mapping Explained
Docker port mapping format: `HOST:CONTAINER`
```yaml
ports:
- "20389:389"
```
This means:
- **20389** = Port on your Mac (accessible via `localhost:20389`)
- **389** = Port inside the Docker container (LDAP's standard port)
The container always listens on 389 internally. We just map it to 20389 externally.
## Testing Multiple Configurations
Run multiple LDAP servers simultaneously:
```bash
# Terminal 1: First instance
cd ldap_docker_1
echo "LDAP_PORT=20389" > .env
echo "LDAPS_PORT=20636" >> .env
make start
# Terminal 2: Second instance
cd ldap_docker_2
echo "LDAP_PORT=21389" > .env
echo "LDAPS_PORT=21636" >> .env
make start
# Now you have two LDAP servers running!
ldapsearch -H ldap://localhost:20389 ... # Server 1
ldapsearch -H ldap://localhost:21389 ... # Server 2
```
## Best Practices
1. **Use .env for configuration** - Don't hardcode ports in scripts
2. **Document your ports** - Add comments in .env explaining your choices
3. **Use unprivileged ports** - Ports above 1024 don't require root
4. **Avoid common ports** - Skip 8080, 3000, 5000 (often used by other tools)
5. **Be consistent** - If LDAP is 20389, make LDAPS 20636 (same prefix)
## Integration Examples
### Configure Your Application
After changing ports, update your application's LDAP configuration:
```python
# Django settings.py
AUTH_LDAP_SERVER_URI = "ldap://localhost:20389"
# Flask
LDAP_HOST = "localhost"
LDAP_PORT = 20389
# Environment variable
export LDAP_URL="ldap://localhost:20389"
```
### Using with Docker Networks
If your application is also in Docker:
```yaml
# Your app's docker-compose.yml
services:
your-app:
environment:
LDAP_URL: "ldap://ldap-server:389" # Use container name and internal port
networks:
- ldap_docker_ldap-network # Connect to LDAP network
networks:
ldap_docker_ldap-network:
external: true
```
## Quick Reference
```bash
# View current configuration
cat .env | grep PORT
# Generate ldapsearch commands
./scripts/ldapsearch_helper.sh
# Test connection on custom port
make test-connection
# Check what ports containers are using
docker-compose ps
# Change ports (example)
echo "LDAP_PORT=12389" > .env
echo "LDAPS_PORT=12636" >> .env
make down && make start
# Verify new ports work
make test-users
```
## Summary
**To use custom ports:**
1. Edit `LDAP_PORT` and `LDAPS_PORT` in `.env`
2. Run `make down && make start`
3. All tools automatically use your new ports!
**Need ldapsearch commands?**
- Run `./scripts/ldapsearch_helper.sh`
- Or manually replace 389 → your LDAP_PORT, 636 → your LDAPS_PORT
That's it! The port configuration system is designed to be simple and automatic.
---
**See Also:**
- [README.md](README.md) - Full documentation
- [GETTING_STARTED.md](GETTING_STARTED.md) - Setup guide
- [QUICKREF.md](QUICKREF.md) - Command reference

View File

@@ -1,354 +0,0 @@
# LDAP Docker Quick Reference
Quick reference for common operations and configurations.
## 🚀 Quick Start
```bash
# Complete setup
make dev-setup
# Or step by step
make install # Install dependencies
make certs-generate # Generate certificates
make start # Start server
make test-connection # Test it works
```
## 🎯 Common Commands
### Server Management
```bash
make start # Start LDAP server
make stop # Stop LDAP server
make restart # Restart LDAP server
make down # Stop and remove containers
make logs # View logs (follow mode)
make status # Check container status
```
### Testing
```bash
make test-connection # Test LDAP connection
make test-auth # Test authentication
make test-users # List all users
make test-all # Run all tests
```
### Certificates
```bash
make certs-generate # Generate self-signed certs
make certs-check # Verify certificates
```
## 🔑 Default Credentials
### Admin Access
- **DN:** `cn=admin,dc=testing,dc=local`
- **Password:** `admin_password`
- **Base DN:** `dc=testing,dc=local`
### phpLDAPadmin
- URL: http://localhost:8080
- Login DN: `cn=admin,dc=testing,dc=local`
- Password: `admin_password`
## 👥 Test Users
All users have password: `password123`
| Username | Full Name | Email | DN |
|----------|-----------|-------|-----|
| `admin` | Admin User | admin@testing.local | `uid=admin,ou=people,dc=testing,dc=local` |
| `jdoe` | John Doe | jdoe@testing.local | `uid=jdoe,ou=people,dc=testing,dc=local` |
| `jsmith` | Jane Smith | jsmith@testing.local | `uid=jsmith,ou=people,dc=testing,dc=local` |
| `testuser` | Test User | testuser@testing.local | `uid=testuser,ou=people,dc=testing,dc=local` |
## 🌐 Service Ports
| Service | Port | URL/Connection |
|---------|------|----------------|
| LDAP | 389 | `ldap://localhost:389` |
| LDAPS | 636 | `ldaps://localhost:636` |
| phpLDAPadmin | 8080 | `http://localhost:8080` |
## 🔍 Common LDAP Queries
### Search All Users
```bash
ldapsearch -H ldap://localhost:389 \
-D "cn=admin,dc=testing,dc=local" \
-w admin_password \
-b "ou=people,dc=testing,dc=local" \
"(objectClass=inetOrgPerson)"
```
### Search Specific User
```bash
ldapsearch -H ldap://localhost:389 \
-D "cn=admin,dc=testing,dc=local" \
-w admin_password \
-b "dc=testing,dc=local" \
"(uid=jdoe)"
```
### Search All Groups
```bash
ldapsearch -H ldap://localhost:389 \
-D "cn=admin,dc=testing,dc=local" \
-w admin_password \
-b "ou=groups,dc=testing,dc=local" \
"(objectClass=groupOfNames)"
```
### Anonymous Bind (Read-Only)
```bash
ldapsearch -H ldap://localhost:389 \
-x \
-b "dc=testing,dc=local" \
"(objectClass=*)"
```
## 🔒 LDAPS/SSL Testing
### Test SSL Connection
```bash
openssl s_client -connect localhost:636 -CAfile certs/ca.crt
```
### LDAPS Search
```bash
ldapsearch -H ldaps://localhost:636 \
-D "cn=admin,dc=testing,dc=local" \
-w admin_password \
-b "dc=testing,dc=local"
```
### Verify Certificate
```bash
openssl verify -CAfile certs/ca.crt certs/server.crt
openssl x509 -in certs/server.crt -text -noout
```
## 🐍 Python LDAP3 Examples
### Simple Connection
```python
from ldap3 import Server, Connection
server = Server('ldap://localhost:389')
conn = Connection(server,
user='cn=admin,dc=testing,dc=local',
password='admin_password',
auto_bind=True)
print("Connected!")
conn.unbind()
```
### Search Users
```python
from ldap3 import Server, Connection
server = Server('ldap://localhost:389')
conn = Connection(server,
user='cn=admin,dc=testing,dc=local',
password='admin_password',
auto_bind=True)
conn.search('dc=testing,dc=local',
'(objectClass=inetOrgPerson)',
attributes=['uid', 'cn', 'mail'])
for entry in conn.entries:
print(f"{entry.cn}: {entry.mail}")
conn.unbind()
```
### Authenticate User
```python
from ldap3 import Server, Connection
server = Server('ldap://localhost:389')
conn = Connection(server,
user='uid=jdoe,ou=people,dc=testing,dc=local',
password='password123')
if conn.bind():
print("Authentication successful!")
else:
print("Authentication failed!")
conn.unbind()
```
## 🐳 Docker Commands
### View Logs
```bash
docker-compose logs -f openldap # Follow LDAP logs
docker-compose logs --tail=100 openldap # Last 100 lines
docker-compose logs phpldapadmin # Admin UI logs
```
### Container Shell Access
```bash
docker-compose exec openldap bash # Shell in LDAP container
docker ps # List running containers
docker-compose ps # List project containers
```
### Volume Management
```bash
docker volume ls # List volumes
docker-compose down -v # Remove volumes (deletes data!)
```
## 🔧 Troubleshooting Quick Fixes
### Server Won't Start
```bash
# Check if ports are in use
lsof -i :389
lsof -i :636
lsof -i :8080
# Check Docker is running
docker version
# View error logs
docker-compose logs openldap
```
### Certificate Errors
```bash
# Verify certificates exist
ls -la certs/
# Regenerate certificates
make certs-generate --force
# Check certificate validity
openssl x509 -in certs/server.crt -noout -dates
```
### Connection Refused
```bash
# Check container is running
docker-compose ps
# Wait for initialization (can take 10-30 seconds)
make logs
# Restart server
make restart
```
### Authentication Fails
```bash
# Verify credentials
# Default: cn=admin,dc=testing,dc=local / admin_password
# Check if users are loaded
make test-users
# View LDAP directory structure
ldapsearch -H ldap://localhost:389 -x -b "dc=testing,dc=local" -s base
```
### Data Not Appearing
```bash
# Check if LDIF files were loaded
docker-compose logs openldap | grep -i ldif
# Rebuild with fresh data
make down-volumes # WARNING: Deletes all data!
make start
```
## 📁 File Locations
### Configuration Files
- `docker-compose.yml` - Docker services configuration
- `pyproject.toml` - Python dependencies
- `.env.example` - Environment variables template
### Data Files
- `ldif/01-users.ldif` - Initial LDAP data
- `certs/` - SSL certificates (git-ignored)
### Scripts
- `scripts/cli.py` - CLI management tool
- `scripts/generate_certs.py` - Certificate generator
- `quickstart.sh` - Interactive setup script
## 🎓 LDAP Basics
### DN (Distinguished Name)
Format: `attribute=value,ou=unit,dc=domain,dc=tld`
Examples:
- `cn=admin,dc=testing,dc=local` - Admin user
- `uid=jdoe,ou=people,dc=testing,dc=local` - Regular user
- `cn=developers,ou=groups,dc=testing,dc=local` - Group
### Common Object Classes
- `inetOrgPerson` - Person with internet attributes
- `posixAccount` - Unix/Linux account
- `groupOfNames` - Group with members
### Common Attributes
- `uid` - User ID (username)
- `cn` - Common Name (full name)
- `sn` - Surname (last name)
- `mail` - Email address
- `userPassword` - Hashed password
- `member` - Group member DN
## 🔗 Useful Links
- [OpenLDAP Documentation](https://www.openldap.org/doc/)
- [LDAP3 Python Library](https://ldap3.readthedocs.io/)
- [RFC 4511 - LDAP Protocol](https://tools.ietf.org/html/rfc4511)
- [phpLDAPadmin](http://phpldapadmin.sourceforge.net/)
## 💡 Tips
1. **Use LDAPS in applications**: Always prefer `ldaps://` over `ldap://`
2. **Test with anonymous bind first**: Use `-x` flag with ldapsearch
3. **Check logs when troubleshooting**: `make logs` is your friend
4. **Certificate hostname must match**: Ensure SAN includes `ldap.testing.local`
5. **Wait after starting**: Server needs 10-30 seconds to initialize
6. **Backup before experimenting**: Use `make down` not `make down-volumes`
---
**Need more help?** See full documentation in README.md

490
README.md
View File

@@ -1,25 +1,31 @@
# LDAP Docker
This is a development tool project that, when deployed, offers an SSL-capable OpenLDAP server populated with test users for a fictional `testing.local` domain.
A pre-configured OpenLDAP server with SSL/TLS support and test users for the `{.env:LDAP_DOMAIN}` domain.
Perfect for local development and testing of applications that need LDAP authentication without setting up a complex infrastructure.
Perfect for local development and testing of applications that need LDAP authentication without setting up a complex infrastructure. Just run `docker-compose up -d` and you're ready to go!
## Features
- 🔒 **SSL/TLS Support** - LDAPS on port 636 with custom certificate support
- 👥 **Pre-populated Users** - Test users and groups ready to use
- 🌐 **Web Admin Interface** - phpLDAPadmin for easy management
- 🐳 **Docker-based** - Easy deployment with Docker Compose
- 🔧 **Python Management Tools** - UV-based CLI for convenience
- 🍎 **Cross-platform** - Works on MacOS (Rancher Desktop), Linux, and Windows
- **SSL/TLS Support** - LDAPS on port `{.env:LDAPS_PORT}` with custom certificate support
- **Pre-populated Users** - Test users and groups ready to use
- **Web Admin Interface** - phpLDAPadmin for easy management
- **Docker-based** - Easy deployment with Docker Compose
- **Fully Configurable** - Customize ports, domain, credentials via `.env` file
- **Optional Management Tools** - Makefile and Python scripts for convenience
- **Cross-platform** - Works anywhere Docker runs
> **📝 Note:** Throughout this documentation, values shown as `{.env:VARIABLE_NAME}` indicate they can be customized via environment variables. See [Configuration](#configuration) for details.
## Table of Contents
- [Prerequisites](#prerequisites)
- [Quick Start](#quick-start)
- [Verify Installation](#verify-installation)
- [Custom Certificates (Dev-CA)](#custom-certificates-dev-ca)
- [Usage](#usage)
- [Configuration](#configuration)
- [Test Users](#test-users)
- [Next Steps](#next-steps)
- [Management Tools](#management-tools)
- [Project Structure](#project-structure)
- [Troubleshooting](#troubleshooting)
@@ -29,67 +35,122 @@ Perfect for local development and testing of applications that need LDAP authent
### Required
- **Docker** or **Rancher Desktop** (recommended for MacOS)
- MacOS: [Download Rancher Desktop](https://rancherdesktop.io/)
- Alternatively: [Docker Desktop](https://www.docker.com/products/docker-desktop)
- **Python 3.9+** (for management scripts)
- **UV Package Manager** (recommended)
- **Docker** - Any provider that can run Docker Compose
- [Docker Desktop](https://www.docker.com/products/docker-desktop) (Windows, Mac, Linux)
- [Rancher Desktop](https://rancherdesktop.io/) (Alternative for Mac/Linux)
- Or native Docker on Linux
- **SSL Certificates** - Either:
- Generate with the included Python script (requires Python 3.9+)
- OR bring your own certificates (see [Custom Certificates](#custom-certificates-dev-ca))
That's it! Everything runs in Docker containers.
### Optional Tools
These tools provide convenient shortcuts but are **not required**:
- **Make** - For using `make start` instead of `docker-compose up -d`
- **Python 3.9+** - For certificate generation script and examples
- **UV Package Manager** - Fast Python dependency management
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
### Optional
- **OpenSSL** (for certificate verification)
- **LDAP utilities** (ldapsearch, ldapadd, etc.) for testing
- **OpenSSL** - For manual certificate verification
- **LDAP utilities** - Command-line tools (ldapsearch, ldapadd, etc.)
## Quick Start
### Option 1: Using Make (Recommended)
### Simple Way (Docker Only)
```bash
# 1. Install dependencies
make install
# 1. Generate certificates (requires Python)
python3 scripts/generate_certs.py
# 2. Generate certificates (or copy your own - see below)
make certs-generate
# 3. Start the server
make start
# 4. Test the connection
make test-connection
# 5. List users
make test-users
```
### Option 2: Using Docker Compose Directly
```bash
# 1. Generate certificates
python scripts/generate_certs.py
# 2. Start services
# 2. Start the server
docker-compose up -d
# 3. View logs
docker-compose logs -f openldap
# 4. Stop the server
docker-compose down
```
### Option 3: Complete Dev Setup
That's it! The server is now running at `ldap://localhost:{.env:LDAP_PORT}` and `ldaps://localhost:{.env:LDAPS_PORT}`.
This will install everything, generate certificates, and start the server:
### Convenient Way (Using Make)
If you have Make installed, use these shortcuts:
```bash
# Complete setup (installs Python deps, generates certs, starts server)
make dev-setup
# Or step by step:
make install # Install Python dependencies
make certs-generate # Generate SSL certificates
make start # Start the server
make verify-users # Verify users are loaded
make logs # View logs
```
Run `make help` to see all available commands.
### Using Your Own Certificates
Instead of generating certificates, copy yours to the `certs/` directory:
```bash
cp /path/to/ca.crt certs/ca.crt
cp /path/to/server.crt certs/server.crt
cp /path/to/server.key certs/server.key
docker-compose up -d
```
> **💡 Tip:** Want to customize ports, passwords, or domain? Copy `.env.example` to `.env` and edit your settings. See the [Configuration](#configuration) section for details.
## Verify Installation
After starting the server, verify everything is working:
### Test 1: List All Users
```bash
make verify-users
```
You should see output like:
```
Found 4 user(s):
- Admin User: admin (admin@{.env:LDAP_DOMAIN})
- John Doe: jdoe (jdoe@{.env:LDAP_DOMAIN})
- Jane Smith: jsmith (jsmith@{.env:LDAP_DOMAIN})
- Test User: testuser (testuser@{.env:LDAP_DOMAIN})
```
### Test 2: Try the Web Interface
Open `http://localhost:{.env:PHPLDAPADMIN_PORT}` in your browser and login with:
- **Login DN:** `cn=admin,{.env:LDAP_BASE_DN}`
- **Password:** `{.env:LDAP_ADMIN_PASSWORD}`
### Test 3: Run the Example Script (Optional - Requires Python)
```bash
python examples/simple_auth.py
```
This authenticates user `jdoe` and displays their information.
## Custom Certificates (Dev-CA)
If you maintain your own dev-ca (as mentioned), you can use your own certificates instead of self-signed ones:
If you wish, you can use your own certificates instead of self-signed ones:
### Using Your Dev-CA Certificates
### Using Your Certificates
```bash
# Copy your certificates to the certs directory
@@ -103,15 +164,15 @@ chmod 644 certs/server.crt
chmod 600 certs/server.key
```
**Important:** The server certificate should be issued for the hostname `ldap.testing.local` or include it as a Subject Alternative Name (SAN).
**Important:** The server certificate should be issued for the hostname `{.env:LDAP_HOSTNAME}` or include it as a Subject Alternative Name (SAN).
### Certificate Requirements
The OpenLDAP container expects three files in the `certs/` directory:
- `ca.crt` - Your CA root certificate
- `server.crt` - Server certificate for ldap.testing.local
- `server.key` - Private key for the server certificate
- `ca.crt` - Your CA root certificate (filename: `{.env:LDAP_TLS_CA_CRT_FILENAME}`)
- `server.crt` - Server certificate for `{.env:LDAP_HOSTNAME}` (filename: `{.env:LDAP_TLS_CRT_FILENAME}`)
- `server.key` - Private key for the server certificate (filename: `{.env:LDAP_TLS_KEY_FILENAME}`)
### Generating Certificates with Your OpenSSL-based Dev-CA
@@ -124,16 +185,16 @@ cd /path/to/your/dev-ca
# Generate server key
openssl genrsa -out ldap-server.key 4096
# Generate certificate signing request
# Generate certificate signing request (use your LDAP_HOSTNAME value)
openssl req -new -key ldap-server.key -out ldap-server.csr \
-subj "/CN=ldap.testing.local"
-subj "/CN={.env:LDAP_HOSTNAME}"
# Sign with your CA (adjust paths as needed)
openssl x509 -req -in ldap-server.csr \
-CA ca-cert.pem -CAkey ca-key.pem \
-CAcreateserial -out ldap-server.crt \
-days 365 -sha256 \
-extfile <(printf "subjectAltName=DNS:ldap.testing.local,DNS:localhost,IP:127.0.0.1")
-extfile <(printf "subjectAltName=DNS:{.env:LDAP_HOSTNAME},DNS:localhost,IP:127.0.0.1")
# Copy to LDAP Docker project
cp ldap-server.crt /path/to/ldap_docker/certs/server.crt
@@ -147,17 +208,19 @@ cp ca-cert.pem /path/to/ldap_docker/certs/ca.crt
Once started, the following services are available:
| Service | URL/Connection | Description |
|---------|----------------|-------------|
| LDAP | `ldap://localhost:389` | Standard LDAP (unencrypted) |
| LDAPS | `ldaps://localhost:636` | LDAP over SSL/TLS |
| phpLDAPadmin | `http://localhost:8080` | Web-based administration |
| Service | URL/Connection | Description |
| ------------ | ----------------------- | --------------------------- |
| LDAP | `ldap://localhost:{.env:LDAP_PORT}` | Standard LDAP (unencrypted) |
| LDAPS | `ldaps://localhost:{.env:LDAPS_PORT}` | LDAP over SSL/TLS |
| phpLDAPadmin | `http://localhost:{.env:PHPLDAPADMIN_PORT}` | Web-based administration |
### Admin Credentials
- **Admin DN:** `cn=admin,dc=testing,dc=local`
- **Password:** `admin_password`
- **Base DN:** `dc=testing,dc=local`
- **Admin DN:** `cn=admin,{.env:LDAP_BASE_DN}`
- **Password:** `{.env:LDAP_ADMIN_PASSWORD}`
- **Base DN:** `{.env:LDAP_BASE_DN}`
> **Note:** Default values shown. Customize these by creating a `.env` file (see [Configuration](#configuration)).
### Common Commands
@@ -174,8 +237,11 @@ make logs
# Check status
make status
# Run tests
make test-all
# Run unit tests
make test
# Verify running server
make verify-all
# Open shell in container
make shell
@@ -184,16 +250,119 @@ make shell
make clean-all
```
## Test Users
## Configuration
You can customize the LDAP server behavior using environment variables. The easiest way is to create a `.env` file in the project root.
### Quick Configuration
```bash
# Copy the example configuration
cp .env.example .env
# Edit with your preferred settings
nano .env # or vim, code, etc.
```
### Available Configuration Options
The `.env.example` file includes all available options with documentation. Below is a comprehensive reference of all environment variables:
| Variable | Default Value | Description | Usage Reference |
|----------|---------------|-------------|-----------------|
| **Domain & Organization** ||||
| `LDAP_DOMAIN` | `testing.local` | Your LDAP domain | Used in: [Services](#accessing-the-services), [Test Users](#default-test-users) |
| `LDAP_ORGANISATION` | `Testing Organization` | Organization name | Used in LDAP server metadata |
| `LDAP_BASE_DN` | `dc=testing,dc=local` | Base Distinguished Name (auto-derived from domain) | Used in: [Services](#accessing-the-services), [Admin Credentials](#admin-credentials), [Test Users](#default-test-users) |
| **Credentials** ||||
| `LDAP_ADMIN_PASSWORD` | `admin_password` | Admin user password | Used in: [Admin Credentials](#admin-credentials), [Verify Installation](#verify-installation) |
| `LDAP_CONFIG_PASSWORD` | `config_password` | Configuration admin password | Used for LDAP server configuration |
| **Ports** ||||
| `LDAP_PORT` | `389` | Standard LDAP port (unencrypted) | Used in: [Services](#accessing-the-services), [Testing Authentication](#testing-authentication) |
| `LDAPS_PORT` | `636` | LDAPS/SSL port (encrypted) | Used in: [Services](#accessing-the-services), [Testing Authentication](#testing-authentication) |
| `PHPLDAPADMIN_PORT` | `8080` | Web UI port | Used in: [Services](#accessing-the-services), [Verify Installation](#verify-installation) |
| **SSL/TLS** ||||
| `LDAP_TLS` | `true` | Enable TLS support | Controls SSL/TLS functionality |
| `LDAP_TLS_CRT_FILENAME` | `server.crt` | Server certificate filename | Used in: [Custom Certificates](#custom-certificates-dev-ca) |
| `LDAP_TLS_KEY_FILENAME` | `server.key` | Server private key filename | Used in: [Custom Certificates](#custom-certificates-dev-ca) |
| `LDAP_TLS_CA_CRT_FILENAME` | `ca.crt` | CA certificate filename | Used in: [Custom Certificates](#custom-certificates-dev-ca) |
| `LDAP_TLS_VERIFY_CLIENT` | `try` | Client certificate verification mode | Options: `never`, `allow`, `try`, `demand` |
| **Container Configuration** ||||
| `LDAP_HOSTNAME` | `ldap.testing.local` | LDAP server hostname | Used in certificate validation |
| `LDAP_CONTAINER_NAME` | `ldap-server` | LDAP container name | Used in Docker commands |
| `PHPLDAPADMIN_CONTAINER_NAME` | `ldap-admin` | phpLDAPadmin container name | Used in Docker commands |
| **Other Options** ||||
| `LDAP_LOG_LEVEL` | `256` | Logging verbosity | Higher = more verbose |
> **Tip:** Throughout this documentation, values shown as `{.env:VARIABLE_NAME}` indicate they are configured via these environment variables.
#### Summary by Category
##### Domain & Organization
- `LDAP_DOMAIN` - Your LDAP domain (default: `testing.local`)
- `LDAP_ORGANISATION` - Organization name (default: `Testing Organization`)
- `LDAP_BASE_DN` - Base DN, auto-derived from domain if not set
##### Credentials
- `LDAP_ADMIN_PASSWORD` - Admin password (default: `admin_password`)
- `LDAP_CONFIG_PASSWORD` - Config admin password (default: `config_password`)
##### Ports
- `LDAP_PORT` - Standard LDAP port (default: `389`)
- `LDAPS_PORT` - LDAPS/SSL port (default: `636`)
- `PHPLDAPADMIN_PORT` - Web UI port (default: `8080`)
##### SSL/TLS
- `LDAP_TLS` - Enable TLS (default: `true`)
- `LDAP_TLS_CRT_FILENAME` - Server cert filename (default: `server.crt`)
- `LDAP_TLS_KEY_FILENAME` - Server key filename (default: `server.key`)
- `LDAP_TLS_CA_CRT_FILENAME` - CA cert filename (default: `ca.crt`)
##### Container Names
- `LDAP_HOSTNAME` - LDAP server hostname (default: `ldap.testing.local`)
- `LDAP_CONTAINER_NAME` - Container name (default: `ldap-server`)
- `PHPLDAPADMIN_CONTAINER_NAME` - Admin UI container (default: `ldap-admin`)
##### Other Options
- `LDAP_LOG_LEVEL` - Logging verbosity (default: `256`)
- `DEBUG` - Enable debug output (default: `false`)
- `TZ` - Timezone (default: `UTC`)
### Example Custom Configuration
```bash
# .env
LDAP_DOMAIN=mycompany.local
LDAP_ORGANISATION=My Company
LDAP_ADMIN_PASSWORD=mysecurepassword
# Use different ports
LDAP_PORT=1389
LDAPS_PORT=1636
PHPLDAPADMIN_PORT=9080
```
After creating or modifying `.env`, restart the containers:
```bash
docker-compose down
docker-compose up -d
```
For a complete list of options with detailed descriptions, see `.env.example`.
## Default Test Users
The LDAP directory is pre-populated with the following test users:
| Username | Full Name | Email | Password | UID |
|----------|-----------|-------|----------|-----|
| admin | Admin User | admin@testing.local | password123 | 10000 |
| jdoe | John Doe | jdoe@testing.local | password123 | 10001 |
| jsmith | Jane Smith | jsmith@testing.local | password123 | 10002 |
| testuser | Test User | testuser@testing.local | password123 | 10003 |
| Username | Full Name | Email | Password | UID |
| -------- | ---------- | -------------------------------- | ----------- | ----- |
| admin | Admin User | admin@`{.env:LDAP_DOMAIN}` | password123 | 10000 |
| jdoe | John Doe | jdoe@`{.env:LDAP_DOMAIN}` | password123 | 10001 |
| jsmith | Jane Smith | jsmith@`{.env:LDAP_DOMAIN}` | password123 | 10002 |
| testuser | Test User | testuser@`{.env:LDAP_DOMAIN}` | password123 | 10003 |
> **Note:** Email addresses are based on `{.env:LDAP_DOMAIN}` (default: `testing.local`). User DNs follow the pattern `uid=username,ou=people,{.env:LDAP_BASE_DN}`.
### Test Groups
@@ -204,57 +373,126 @@ The LDAP directory is pre-populated with the following test users:
### Testing Authentication
```bash
# Test with ldapsearch
ldapsearch -H ldap://localhost:389 \
-D "uid=jdoe,ou=people,dc=testing,dc=local" \
# Test with ldapsearch (using default ports from .env)
ldapsearch -H ldap://localhost:{.env:LDAP_PORT} \
-D "uid=jdoe,ou=people,{.env:LDAP_BASE_DN}" \
-w password123 \
-b "dc=testing,dc=local" \
-b "{.env:LDAP_BASE_DN}" \
"(uid=jdoe)"
# Test LDAPS with SSL
ldapsearch -H ldaps://localhost:636 \
-D "uid=jdoe,ou=people,dc=testing,dc=local" \
ldapsearch -H ldaps://localhost:{.env:LDAPS_PORT} \
-D "uid=jdoe,ou=people,{.env:LDAP_BASE_DN}" \
-w password123 \
-b "dc=testing,dc=local" \
-b "{.env:LDAP_BASE_DN}" \
"(uid=jdoe)"
# Best practice: Test auth by searching for user's own entry
# This works for all users, not just admin
ldapsearch -H ldap://localhost:{.env:LDAP_PORT} \
-D "uid=jdoe,ou=people,{.env:LDAP_BASE_DN}" \
-w password123 \
-b "uid=jdoe,ou=people,{.env:LDAP_BASE_DN}" \
-s base \
"(objectClass=*)"
```
## Next Steps
Now that your LDAP server is running, you can:
### 1. Integrate with Your Application
Point your application to the LDAP server:
- **LDAP URL:** `ldap://localhost:{.env:LDAP_PORT}`
- **LDAPS URL:** `ldaps://localhost:{.env:LDAPS_PORT}`
- **Base DN:** `{.env:LDAP_BASE_DN}`
See `examples/README.md` for code samples and integration patterns.
### 2. Add Custom Users
Edit `ldif/01-users.ldif` to add more users or modify existing ones, then reload:
```bash
make down-volumes # WARNING: Deletes all data
make start
```
### 3. Customize Configuration
Create a `.env` file to customize ports, credentials, and behavior:
```bash
cp .env.example .env
# Edit .env with your settings
docker-compose down && docker-compose up -d
```
See the [Configuration](#configuration) section for all available options.
### 4. Learn More
- `certs/README.md` - Certificate management guide
- `examples/README.md` - Integration examples and patterns
- [OpenLDAP Documentation](https://www.openldap.org/doc/)
## Management Tools
This project includes Python-based management tools using UV.
You can manage the LDAP server using `docker-compose` commands directly, or use the included Makefile for convenience.
### Installation
### Using Docker Compose (No Additional Tools Required)
```bash
# Install dependencies with UV
make install
# Server management
docker-compose up -d # Start server
docker-compose stop # Stop server
docker-compose restart # Restart server
docker-compose logs -f openldap # View logs
docker-compose ps # Check status
docker-compose down # Remove containers
docker-compose down -v # Remove containers and data
# Or manually
uv sync
# Generate certificates (requires Python)
python3 scripts/generate_certs.py
```
### CLI Tool Usage
### Using Make (Optional Convenience)
If you have Make installed, use these shortcuts:
```bash
# View available commands
uv run ldap-docker --help
make help # View all available commands
# Server management
uv run ldap-docker server start
uv run ldap-docker server stop
uv run ldap-docker server logs -f
make start # Start the LDAP server
make stop # Stop the LDAP server
make restart # Restart the LDAP server
make logs # View server logs
make status # Check server status
# Certificate management
uv run ldap-docker certs generate
uv run ldap-docker certs check
make certs-generate # Generate SSL certificates
make certs-check # Verify SSL certificates
# Testing
uv run ldap-docker test connection
uv run ldap-docker test auth
uv run ldap-docker test users
# Unit Testing (requires Python + pytest)
make test # Run unit tests
make test-cov # Run tests with coverage
# Initialize environment
uv run ldap-docker init
# Verification (requires running container + ldap3)
make verify-connection # Verify LDAP connection
make verify-auth # Verify authentication
make verify-users # List all users
make verify-all # Run all verification checks
# Setup
make install # Install Python dependencies
make dev-setup # Complete development setup
# Cleanup
make clean # Clean build artifacts
make down-volumes # Remove containers and data
```
## Project Structure
@@ -269,7 +507,6 @@ ldap_docker/
├── ldif/ # LDAP Data Interchange Format files
│ └── 01-users.ldif # Initial user and group data
├── scripts/ # Management scripts
│ ├── cli.py # CLI tool for managing LDAP
│ └── generate_certs.py # Certificate generation utility
├── docker-compose.yml # Docker Compose configuration
├── pyproject.toml # Python project configuration (UV)
@@ -332,14 +569,14 @@ docker-compose ps
make logs
# Test basic connectivity
telnet localhost 389
telnet localhost {.env:LDAP_PORT}
```
**Problem:** Authentication fails
```bash
# Verify credentials
# Default admin: cn=admin,dc=testing,dc=local / admin_password
# Default admin: cn=admin,{.env:LDAP_BASE_DN} / {.env:LDAP_ADMIN_PASSWORD}
# Check LDAP logs
docker-compose logs openldap | grep -i error
@@ -365,14 +602,14 @@ make start
Edit `ldif/01-users.ldif` to add more users or modify existing ones:
```ldif
dn: uid=newuser,ou=people,dc=testing,dc=local
dn: uid=newuser,ou=people,{.env:LDAP_BASE_DN}
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: newuser
cn: New User
sn: User
mail: newuser@testing.local
mail: newuser@{.env:LDAP_DOMAIN}
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ=
uidNumber: 10004
gidNumber: 10004
@@ -380,6 +617,8 @@ homeDirectory: /home/newuser
loginShell: /bin/bash
```
> **Note:** Replace `{.env:LDAP_BASE_DN}` and `{.env:LDAP_DOMAIN}` with your actual values from the `.env` file (defaults: `dc=testing,dc=local` and `testing.local`).
Then restart with fresh data:
```bash
@@ -389,39 +628,43 @@ make start
### Modifying Configuration
Edit `docker-compose.yml` to change:
- Port mappings
- Environment variables
- Volume mounts
- Resource limits
### Python Development
The easiest way to customize settings is with a `.env` file:
```bash
# Install development dependencies
cp .env.example .env
# Edit .env with your preferences
docker-compose down && docker-compose up -d
```
See `.env.example` for all available options with detailed documentation, or the [Configuration](#configuration) section.
For advanced customization, you can also edit `docker-compose.yml` to change volume mounts, resource limits, or other Docker-specific settings.
### Python Development (Optional)
If you're contributing to the Python scripts:
```bash
# Install development dependencies (requires UV)
make install-dev
# Run tests
uv run pytest
# Format code
uv run black scripts/
# Lint code
uv run ruff check scripts/
# Type check
uv run mypy scripts/
# Format and lint code
ruff format scripts/
ruff check scripts/
```
### Building on Other Platforms
### Cross-Platform Compatibility
This project is designed to work on:
- **MacOS** (with Rancher Desktop or Docker Desktop)
- **Linux** (with Docker and Docker Compose)
- **Windows** (with Docker Desktop and WSL2)
This project uses standard Docker images and docker-compose.yml format, so it works anywhere Docker runs:
The `docker-compose.yml` uses standard Docker images and should work anywhere Docker runs.
- **Linux** - Native Docker
- **MacOS** - Docker Desktop, Rancher Desktop, or Colima
- **Windows** - Docker Desktop (with WSL2)
No platform-specific code or configuration needed!
## Security Notes
@@ -442,6 +685,7 @@ MIT License - See LICENSE file for details
## Contributing
Contributions are welcome! This is a development tool, so:
1. Keep it simple and easy to use
2. Maintain cross-platform compatibility
3. Update documentation for any changes

View File

@@ -6,13 +6,13 @@ This directory should contain your SSL/TLS certificates for the LDAP server.
The OpenLDAP container expects the following files in this directory:
- `ca.crt` - Certificate Authority certificate (your dev-ca root certificate)
- `server.crt` - Server certificate for ldap.testing.local
- `server.key` - Private key for the server certificate
- `ca.crt` - Certificate Authority certificate (your dev-ca root certificate) - filename: `{.env:LDAP_TLS_CA_CRT_FILENAME}`
- `server.crt` - Server certificate for `{.env:LDAP_HOSTNAME}` - filename: `{.env:LDAP_TLS_CRT_FILENAME}`
- `server.key` - Private key for the server certificate - filename: `{.env:LDAP_TLS_KEY_FILENAME}`
## Using Your Custom Dev-CA Certificates
If you maintain your own dev-ca (as mentioned), simply copy your certificates here:
If you maintain your own dev-ca, simply copy your certificates here:
```bash
# Copy your dev-ca generated certificates to this directory
@@ -22,9 +22,9 @@ cp /path/to/your/dev-ca/ca-cert.pem ./ca.crt
```
**Important Notes:**
- The server certificate should be issued for the hostname `ldap.testing.local`
- The server certificate should be issued for the hostname `{.env:LDAP_HOSTNAME}` (default: `ldap.testing.local`)
- The certificate can also include SANs (Subject Alternative Names) like:
- `DNS:ldap.testing.local`
- `DNS:{.env:LDAP_HOSTNAME}`
- `DNS:localhost`
- `IP:127.0.0.1`
- Ensure the private key is readable by the container (permissions should be 600 or 644)
@@ -46,13 +46,13 @@ openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
# Generate server private key
openssl genrsa -out server.key 4096
# Generate server certificate signing request
# Generate server certificate signing request (use your LDAP_HOSTNAME value)
openssl req -new -key server.key -out server.csr \
-subj "/C=US/ST=State/L=City/O=Testing Org/CN=ldap.testing.local"
-subj "/C=US/ST=State/L=City/O=Testing Org/CN={.env:LDAP_HOSTNAME}"
# Create extensions file for SAN
# Create extensions file for SAN (use your LDAP_HOSTNAME value)
cat > server-ext.cnf <<EOF
subjectAltName = DNS:ldap.testing.local,DNS:localhost,IP:127.0.0.1
subjectAltName = DNS:{.env:LDAP_HOSTNAME},DNS:localhost,IP:127.0.0.1
extendedKeyUsage = serverAuth,clientAuth
EOF
@@ -104,12 +104,12 @@ openssl rsa -noout -modulus -in server.key | openssl md5
Once the container is running with your certificates:
```bash
# Test LDAPS connection (port 636)
openssl s_client -connect localhost:636 -CAfile certs/ca.crt
# Test LDAPS connection
openssl s_client -connect localhost:{.env:LDAPS_PORT} -CAfile certs/ca.crt
# Test with ldapsearch
ldapsearch -H ldaps://localhost:636 -x -b "dc=testing,dc=local" \
-D "cn=admin,dc=testing,dc=local" -w admin_password
ldapsearch -H ldaps://localhost:{.env:LDAPS_PORT} -x -b "{.env:LDAP_BASE_DN}" \
-D "cn=admin,{.env:LDAP_BASE_DN}" -w {.env:LDAP_ADMIN_PASSWORD}
```
## Troubleshooting
@@ -117,7 +117,7 @@ ldapsearch -H ldaps://localhost:636 -x -b "dc=testing,dc=local" \
### Certificate Errors
If you see TLS/SSL errors in the logs:
1. Verify the certificate hostname matches `ldap.testing.local`
1. Verify the certificate hostname matches `{.env:LDAP_HOSTNAME}` (default: `ldap.testing.local`)
2. Check that all three files are present and readable
3. Ensure the server certificate is signed by the CA
4. Check certificate expiration dates
@@ -131,4 +131,4 @@ If the container fails to start:
## Security Note
These certificates are for **development use only**. Never use self-signed or development certificates in production environments.
These certificates are for **development use only**. Never use self-signed or development certificates in production environments.

View File

@@ -2,6 +2,8 @@
This directory contains example scripts and applications demonstrating how to use the LDAP server for authentication and user management.
> **📝 Note:** In the examples below, values shown as `{.env:VARIABLE_NAME}` are configurable via environment variables in your `.env` file. The actual default values are: `LDAP_PORT=389`, `LDAPS_PORT=636`, `LDAP_BASE_DN=dc=testing,dc=local`, and `LDAP_ADMIN_PASSWORD=admin_password`. See the main [Configuration](../README.md#configuration) section for all available options.
## Available Examples
### 1. Simple Authentication (`simple_auth.py`)
@@ -9,6 +11,7 @@ This directory contains example scripts and applications demonstrating how to us
A Python script demonstrating basic LDAP authentication and user information retrieval.
**Features:**
- Authenticate users with username/password
- Retrieve detailed user information
- Get user group memberships
@@ -27,14 +30,14 @@ python examples/simple_auth.py --username jsmith --password password123
python examples/simple_auth.py --list-users
# Use a different LDAP server
python examples/simple_auth.py --server ldaps://localhost:636
python examples/simple_auth.py --server ldaps://localhost:{.env:LDAPS_PORT}
```
**Example Output:**
```
🔐 LDAP Authentication Example
Server: ldap://localhost:389
LDAP Authentication Example
Server: ldap://localhost:{.env:LDAP_PORT}
Attempting to authenticate user: jdoe
✅ Authentication successful for user: jdoe
@@ -52,7 +55,7 @@ Last Name: Doe
Email: jdoe@testing.local
UID Number: 10001
GID Number: 10001
DN: uid=jdoe,ou=people,dc=testing,dc=local
DN: uid=jdoe,ou=people,{.env:LDAP_BASE_DN}
==================================================
Fetching user groups...
@@ -69,17 +72,17 @@ User belongs to 2 group(s):
from ldap3 import Server, Connection
# Connect and authenticate
server = Server('ldap://localhost:389')
server = Server('ldap://localhost:{.env:LDAP_PORT}')
conn = Connection(
server,
user='uid=jdoe,ou=people,dc=testing,dc=local',
user='uid=jdoe,ou=people,{.env:LDAP_BASE_DN}',
password='password123',
auto_bind=True
)
# Search for users
conn.search(
'dc=testing,dc=local',
'{.env:LDAP_BASE_DN}',
'(objectClass=inetOrgPerson)',
attributes=['uid', 'cn', 'mail']
)
@@ -94,243 +97,17 @@ conn.unbind()
```bash
# Search for a user
ldapsearch -H ldap://localhost:389 \
-D "cn=admin,dc=testing,dc=local" \
-w admin_password \
-b "dc=testing,dc=local" \
ldapsearch -H ldap://localhost:{.env:LDAP_PORT} \
-D "cn=admin,{.env:LDAP_BASE_DN}" \
-w {.env:LDAP_ADMIN_PASSWORD} \
-b "{.env:LDAP_BASE_DN}" \
"(uid=jdoe)"
# List all users
ldapsearch -H ldap://localhost:389 \
-D "cn=admin,dc=testing,dc=local" \
-w admin_password \
-b "ou=people,dc=testing,dc=local" \
ldapsearch -H ldap://localhost:{.env:LDAP_PORT} \
-D "cn=admin,{.env:LDAP_BASE_DN}" \
-w {.env:LDAP_ADMIN_PASSWORD} \
-b "ou=people,{.env:LDAP_BASE_DN}" \
"(objectClass=inetOrgPerson)" \
uid cn mail
```
### Web Application Integration
#### Flask Example
```python
from flask import Flask, request, jsonify
from ldap3 import Server, Connection
app = Flask(__name__)
@app.route('/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
server = Server('ldap://localhost:389')
user_dn = f'uid={username},ou=people,dc=testing,dc=local'
try:
conn = Connection(server, user=user_dn, password=password)
if conn.bind():
return jsonify({'status': 'success', 'message': 'Authenticated'})
else:
return jsonify({'status': 'error', 'message': 'Invalid credentials'}), 401
except:
return jsonify({'status': 'error', 'message': 'Authentication failed'}), 401
```
#### Django Example
```python
# settings.py
import ldap
from django_auth_ldap.config import LDAPSearch
AUTH_LDAP_SERVER_URI = "ldap://localhost:389"
AUTH_LDAP_BIND_DN = "cn=admin,dc=testing,dc=local"
AUTH_LDAP_BIND_PASSWORD = "admin_password"
AUTH_LDAP_USER_SEARCH = LDAPSearch(
"ou=people,dc=testing,dc=local",
ldap.SCOPE_SUBTREE,
"(uid=%(user)s)"
)
AUTHENTICATION_BACKENDS = [
'django_auth_ldap.backend.LDAPBackend',
'django.contrib.auth.backends.ModelBackend',
]
```
## Common Integration Patterns
### 1. Simple Bind Authentication
The most straightforward approach - try to bind with user credentials:
```python
def authenticate_user(username, password):
server = Server('ldap://localhost:389')
user_dn = f'uid={username},ou=people,dc=testing,dc=local'
conn = Connection(server, user=user_dn, password=password)
return conn.bind()
```
### 2. Search and Bind
Search for the user first, then authenticate:
```python
def authenticate_user(username, password):
# First, search for the user with admin credentials
server = Server('ldap://localhost:389')
admin_conn = Connection(
server,
user='cn=admin,dc=testing,dc=local',
password='admin_password',
auto_bind=True
)
admin_conn.search(
'ou=people,dc=testing,dc=local',
f'(uid={username})',
attributes=['dn']
)
if not admin_conn.entries:
return False
user_dn = admin_conn.entries[0].entry_dn
admin_conn.unbind()
# Now authenticate as the user
user_conn = Connection(server, user=user_dn, password=password)
return user_conn.bind()
```
### 3. Group-Based Authorization
Check if user belongs to specific groups:
```python
def user_has_role(username, required_group):
server = Server('ldap://localhost:389')
conn = Connection(
server,
user='cn=admin,dc=testing,dc=local',
password='admin_password',
auto_bind=True
)
user_dn = f'uid={username},ou=people,dc=testing,dc=local'
conn.search(
'ou=groups,dc=testing,dc=local',
f'(&(objectClass=groupOfNames)(member={user_dn})(cn={required_group}))',
attributes=['cn']
)
return len(conn.entries) > 0
```
## Testing Your Integration
### 1. Start the LDAP Server
```bash
make start
```
### 2. Test Connection
```bash
python examples/simple_auth.py --list-users
```
### 3. Test Authentication
```bash
python examples/simple_auth.py --username jdoe --password password123
```
### 4. Test with Your Application
Point your application to:
- LDAP URL: `ldap://localhost:389`
- LDAPS URL: `ldaps://localhost:636` (with SSL)
- Base DN: `dc=testing,dc=local`
## Available Test Accounts
| Username | Password | Groups | Purpose |
|----------|----------|--------|---------|
| admin | password123 | admins | Administrative testing |
| jdoe | password123 | developers, users | Regular user testing |
| jsmith | password123 | developers, users | Regular user testing |
| testuser | password123 | users | Basic user testing |
## SSL/TLS Configuration
For production-like testing with LDAPS:
```python
import ssl
from ldap3 import Server, Connection, Tls
tls = Tls(
ca_certs_file='certs/ca.crt',
validate=ssl.CERT_REQUIRED
)
server = Server('ldaps://localhost:636', use_ssl=True, tls=tls)
conn = Connection(server, user=user_dn, password=password, auto_bind=True)
```
## Troubleshooting
### Connection Refused
```bash
# Check if LDAP server is running
make status
# Start if not running
make start
```
### Authentication Fails
```bash
# Verify user exists
make test-users
# Check LDAP logs
make logs
```
### Python ImportError
```bash
# Install ldap3 library
uv pip install ldap3
# or
pip install ldap3
```
## Additional Resources
- [ldap3 Documentation](https://ldap3.readthedocs.io/)
- [LDAP Protocol Overview](https://ldap.com/ldap-protocol/)
- [Django LDAP Authentication](https://django-auth-ldap.readthedocs.io/)
- [Flask-LDAP3-Login](https://flask-ldap3-login.readthedocs.io/)
## Contributing Examples
Have an example for a specific framework or use case? Contributions are welcome!
Examples we'd love to see:
- Express.js / Node.js authentication
- Ruby on Rails integration
- Go LDAP client
- Java Spring Security LDAP
- PHP authentication
- Docker Compose with application stack
Submit a pull request with your example!

View File

@@ -13,11 +13,10 @@ Usage:
import argparse
import os
import sys
from typing import Dict, List, Optional
try:
from ldap3 import ALL, Connection, Server
from ldap3.core.exceptions import LDAPException, LDAPBindError
from ldap3.core.exceptions import LDAPBindError, LDAPException
except ImportError:
print("Error: ldap3 library not found.")
print("Install it with: uv pip install ldap3")
@@ -27,9 +26,9 @@ except ImportError:
# Configuration
LDAP_PORT = os.environ.get("LDAP_PORT", "389")
LDAP_SERVER = f"ldap://localhost:{LDAP_PORT}"
LDAP_BASE_DN = "dc=testing,dc=local"
LDAP_PEOPLE_OU = "ou=people,dc=testing,dc=local"
LDAP_GROUPS_OU = "ou=groups,dc=testing,dc=local"
LDAP_BASE_DN = os.environ.get("LDAP_BASE_DN", "dc=testing,dc=local")
LDAP_PEOPLE_OU = f"ou=people,{LDAP_BASE_DN}"
LDAP_GROUPS_OU = f"ou=groups,{LDAP_BASE_DN}"
class LDAPAuthenticator:
@@ -66,8 +65,8 @@ class LDAPAuthenticator:
print(f"❌ Authentication failed for user: {username}")
return False
except LDAPBindError as e:
print(f"❌ Authentication failed: Invalid credentials")
except LDAPBindError:
print("❌ Authentication failed: Invalid credentials")
return False
except LDAPException as e:
print(f"❌ LDAP error: {e}")
@@ -76,7 +75,7 @@ class LDAPAuthenticator:
print(f"❌ Unexpected error: {e}")
return False
def get_user_info(self, username: str, admin_dn: str, admin_password: str) -> Optional[Dict]:
def get_user_info(self, username: str, admin_dn: str, admin_password: str) -> dict | None:
"""
Retrieve detailed information about a user.
@@ -121,7 +120,7 @@ class LDAPAuthenticator:
print(f"Error retrieving user info: {e}")
return None
def get_user_groups(self, username: str, admin_dn: str, admin_password: str) -> List[str]:
def get_user_groups(self, username: str, admin_dn: str, admin_password: str) -> list[str]:
"""
Get all groups that a user belongs to.
@@ -154,7 +153,7 @@ class LDAPAuthenticator:
print(f"Error retrieving user groups: {e}")
return []
def list_all_users(self, admin_dn: str, admin_password: str) -> List[Dict]:
def list_all_users(self, admin_dn: str, admin_password: str) -> list[dict]:
"""
List all users in the directory.
@@ -192,7 +191,7 @@ class LDAPAuthenticator:
return []
def print_user_info(user_info: Dict):
def print_user_info(user_info: dict):
"""Pretty print user information."""
print("\n" + "=" * 50)
print("USER INFORMATION")
@@ -225,10 +224,10 @@ def main():
args = parser.parse_args()
# Admin credentials for retrieving user info
admin_dn = "cn=admin,dc=testing,dc=local"
admin_password = "admin_password"
admin_dn = os.environ.get("LDAP_ADMIN_DN", f"cn=admin,{LDAP_BASE_DN}")
admin_password = os.environ.get("LDAP_ADMIN_PASSWORD", "admin_password")
print(f"\n🔐 LDAP Authentication Example")
print("\nLDAP Authentication Example")
print(f"Server: {args.server}\n")
# Initialize authenticator

View File

@@ -1,7 +1,7 @@
[project]
name = "ldap-docker"
version = "0.1.0"
description = "A development tool for running OpenLDAP with SSL in Docker"
description = "Docker-based OpenLDAP server with SSL/TLS support and test users for development"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
@@ -12,44 +12,22 @@ authors = [
dependencies = [
"ldap3>=2.9.1",
"cryptography>=41.0.0",
"click>=8.1.0",
"python-dotenv>=1.0.0",
"pyyaml>=6.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.5.0",
]
[project.scripts]
ldap-docker = "scripts.cli:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["scripts", "tests"]
packages = ["scripts"]
[dependency-groups]
dev = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"black>=23.0.0",
"ruff>=0.1.0",
"mypy>=1.5.0",
]
[tool.black]
line-length = 100
target-version = ["py39", "py310", "py311", "py312"]
include = '\.pyi?$'
[tool.ruff]
line-length = 100
target-version = "py39"
@@ -63,20 +41,13 @@ select = [
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by black)
"E501", # line too long (handled by ruff format)
"B008", # do not perform function calls in argument defaults
]
[tool.ruff.per-file-ignores]
"__init__.py" = ["F401"]
[tool.mypy]
python_version = "3.9"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = false
ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]

View File

@@ -1,381 +0,0 @@
#!/usr/bin/env bash
#
# Quick Start Script for LDAP Docker
# This script will guide you through setting up and starting the LDAP server
#
set -e # Exit on error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Emoji support (works on most terminals)
CHECK_MARK="✅"
CROSS_MARK="❌"
WARNING="⚠️"
ROCKET="🚀"
GEAR="⚙️"
# Get the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd "$SCRIPT_DIR"
# Functions
print_header() {
echo -e "${BLUE}================================================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}================================================${NC}"
echo ""
}
print_success() {
echo -e "${GREEN}${CHECK_MARK} $1${NC}"
}
print_error() {
echo -e "${RED}${CROSS_MARK} $1${NC}"
}
print_warning() {
echo -e "${YELLOW}${WARNING} $1${NC}"
}
print_info() {
echo -e "${BLUE}${GEAR} $1${NC}"
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Check Docker
check_docker() {
print_info "Checking Docker..."
if command_exists docker; then
if docker version >/dev/null 2>&1; then
print_success "Docker is installed and running"
return 0
else
print_error "Docker is installed but not running"
echo ""
echo "Please start Docker or Rancher Desktop and try again."
return 1
fi
else
print_error "Docker is not installed"
echo ""
echo "Please install one of the following:"
echo " - Rancher Desktop (recommended for MacOS): https://rancherdesktop.io/"
echo " - Docker Desktop: https://www.docker.com/products/docker-desktop"
return 1
fi
}
# Check docker-compose
check_docker_compose() {
print_info "Checking docker-compose..."
if command_exists docker-compose; then
print_success "docker-compose is available"
return 0
elif docker compose version >/dev/null 2>&1; then
print_success "docker compose (v2) is available"
return 0
else
print_warning "docker-compose not found"
echo "Docker Compose is usually included with Docker Desktop and Rancher Desktop."
echo "If you're using a standalone Docker installation, please install docker-compose."
return 1
fi
}
# Check Python
check_python() {
print_info "Checking Python..."
if command_exists python3; then
PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
print_success "Python ${PYTHON_VERSION} is installed"
return 0
elif command_exists python; then
PYTHON_VERSION=$(python --version | cut -d' ' -f2)
print_success "Python ${PYTHON_VERSION} is installed"
return 0
else
print_error "Python is not installed"
echo "Please install Python 3.9 or higher."
return 1
fi
}
# Check/Install UV
check_install_uv() {
print_info "Checking UV package manager..."
if command_exists uv; then
UV_VERSION=$(uv --version | cut -d' ' -f2 || echo "unknown")
print_success "UV ${UV_VERSION} is installed"
return 0
else
print_warning "UV is not installed"
echo ""
echo "UV is a fast Python package manager (recommended for this project)."
read -p "Would you like to install UV now? [Y/n] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
print_info "Installing UV..."
curl -LsSf https://astral.sh/uv/install.sh | sh
# Source the shell config to get UV in PATH
export PATH="$HOME/.cargo/bin:$PATH"
if command_exists uv; then
print_success "UV installed successfully"
return 0
else
print_error "UV installation may have completed but it's not in PATH"
echo "Please restart your terminal and run this script again."
return 1
fi
else
print_warning "Skipping UV installation"
echo "You can install dependencies manually with pip if needed."
return 1
fi
fi
}
# Check certificates
check_certificates() {
print_info "Checking SSL certificates..."
if [[ -f "certs/ca.crt" && -f "certs/server.crt" && -f "certs/server.key" ]]; then
print_success "SSL certificates found"
return 0
else
print_warning "SSL certificates not found"
return 1
fi
}
# Generate certificates
generate_certificates() {
print_info "Generating SSL certificates..."
if command_exists uv && [[ -f "scripts/generate_certs.py" ]]; then
uv run python scripts/generate_certs.py
print_success "Certificates generated"
elif command_exists python3; then
python3 scripts/generate_certs.py
print_success "Certificates generated"
else
print_error "Cannot generate certificates - Python not available"
return 1
fi
}
# Install dependencies
install_dependencies() {
print_info "Installing Python dependencies..."
if command_exists uv; then
uv sync
print_success "Dependencies installed with UV"
elif command_exists python3 && command_exists pip3; then
pip3 install -e .
print_success "Dependencies installed with pip"
else
print_warning "Cannot install dependencies automatically"
echo "Please install dependencies manually."
return 1
fi
}
# Start LDAP server
start_server() {
print_info "Starting LDAP server..."
if command_exists docker-compose; then
docker-compose up -d
else
docker compose up -d
fi
print_success "LDAP server started"
# Wait a bit for the server to initialize
print_info "Waiting for server to initialize (10 seconds)..."
sleep 10
}
# Test connection
test_connection() {
print_info "Testing LDAP connection..."
# Simple check if container is running
if docker ps | grep -q "ldap-server"; then
print_success "LDAP server container is running"
return 0
else
print_warning "LDAP server container may not be fully started yet"
return 1
fi
}
# Print final information
print_final_info() {
echo ""
print_header "${ROCKET} LDAP Server is Ready! ${ROCKET}"
echo "Services are now available at:"
echo ""
echo " ${GREEN}LDAP:${NC} ldap://localhost:389"
echo " ${GREEN}LDAPS:${NC} ldaps://localhost:636"
echo " ${GREEN}Admin UI:${NC} http://localhost:8080"
echo ""
echo "Admin Credentials:"
echo " ${BLUE}DN:${NC} cn=admin,dc=testing,dc=local"
echo " ${BLUE}Password:${NC} admin_password"
echo ""
echo "Test Users (password: password123):"
echo " - jdoe (John Doe)"
echo " - jsmith (Jane Smith)"
echo " - testuser (Test User)"
echo ""
echo "Useful Commands:"
echo " ${BLUE}make logs${NC} - View server logs"
echo " ${BLUE}make test-users${NC} - List all users"
echo " ${BLUE}make status${NC} - Check server status"
echo " ${BLUE}make stop${NC} - Stop the server"
echo " ${BLUE}make help${NC} - See all available commands"
echo ""
echo "Documentation:"
echo " - README.md for full documentation"
echo " - certs/README.md for certificate management"
echo ""
}
# Main script
main() {
clear
print_header "${ROCKET} LDAP Docker Quick Start ${ROCKET}"
echo "This script will set up and start your LDAP development server."
echo ""
# Step 1: Check prerequisites
print_header "Step 1: Checking Prerequisites"
PREREQ_OK=true
check_docker || PREREQ_OK=false
check_docker_compose || PREREQ_OK=false
check_python || PREREQ_OK=false
if [ "$PREREQ_OK" = false ]; then
echo ""
print_error "Some prerequisites are missing. Please install them and try again."
exit 1
fi
echo ""
print_success "All prerequisites are met!"
echo ""
# Step 2: UV installation (optional but recommended)
print_header "Step 2: Package Manager Setup"
check_install_uv
HAS_UV=$?
echo ""
# Step 3: SSL Certificates
print_header "Step 3: SSL Certificate Setup"
if check_certificates; then
echo ""
print_info "Existing certificates will be used."
read -p "Regenerate certificates? [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
generate_certificates
fi
else
echo ""
echo "SSL certificates are required for LDAPS (secure LDAP)."
echo ""
echo "You can:"
echo " 1. Generate self-signed certificates now (recommended for quick start)"
echo " 2. Copy certificates from your dev-ca manually to certs/"
echo ""
read -p "Generate self-signed certificates now? [Y/n] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
generate_certificates
else
print_info "Please copy your certificates to certs/ directory:"
echo " - certs/ca.crt"
echo " - certs/server.crt"
echo " - certs/server.key"
echo ""
read -p "Press Enter when ready to continue..."
if ! check_certificates; then
print_error "Certificates still not found. Exiting."
exit 1
fi
fi
fi
echo ""
# Step 4: Install dependencies (optional)
if [ $HAS_UV -eq 0 ]; then
print_header "Step 4: Installing Dependencies"
install_dependencies
echo ""
else
print_info "Skipping dependency installation (UV not available)"
echo ""
fi
# Step 5: Start server
print_header "Step 5: Starting LDAP Server"
start_server
echo ""
# Step 6: Test
print_header "Step 6: Verifying Installation"
test_connection
echo ""
# Success!
print_final_info
# Offer to view logs
echo ""
read -p "View server logs now? [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo ""
print_info "Showing logs (press Ctrl+C to exit)..."
sleep 2
if command_exists docker-compose; then
docker-compose logs -f openldap
else
docker compose logs -f openldap
fi
fi
}
# Run main function
main
exit 0

View File

@@ -5,9 +5,7 @@ This package contains management and utility scripts for the LDAP Docker
development environment.
Modules:
- cli: Command-line interface for managing LDAP server
- generate_certs: SSL/TLS certificate generation utility
"""
__version__ = "0.1.0"
__all__ = ["cli", "generate_certs"]

View File

@@ -1,464 +0,0 @@
#!/usr/bin/env python3
"""
CLI tool for managing the LDAP Docker development environment.
This tool provides convenient commands for starting, stopping, and testing
the LDAP server, as well as managing test users and certificates.
"""
import os
import subprocess
import sys
import time
from pathlib import Path
from typing import Optional
import click
try:
import ldap3
from ldap3 import ALL, Connection, Server
from ldap3.core.exceptions import LDAPException
except ImportError:
ldap3 = None
# Constants
PROJECT_ROOT = Path(__file__).parent.parent
CERTS_DIR = PROJECT_ROOT / "certs"
LDIF_DIR = PROJECT_ROOT / "ldif"
DEFAULT_HOST = "localhost"
DEFAULT_PORT = int(os.environ.get("LDAP_PORT", "389"))
DEFAULT_LDAPS_PORT = int(os.environ.get("LDAPS_PORT", "636"))
DEFAULT_BASE_DN = "dc=testing,dc=local"
DEFAULT_ADMIN_DN = "cn=admin,dc=testing,dc=local"
DEFAULT_ADMIN_PASSWORD = "admin_password"
def run_command(cmd: list[str], cwd: Optional[Path] = None, check: bool = True) -> subprocess.CompletedProcess:
"""Run a shell command and return the result."""
if cwd is None:
cwd = PROJECT_ROOT
try:
result = subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
check=check,
)
return result
except subprocess.CalledProcessError as e:
click.echo(f"Error running command: {' '.join(cmd)}", err=True)
click.echo(f"Exit code: {e.returncode}", err=True)
if e.stdout:
click.echo(f"stdout: {e.stdout}", err=True)
if e.stderr:
click.echo(f"stderr: {e.stderr}", err=True)
raise
def check_docker():
"""Check if Docker is available."""
try:
result = run_command(["docker", "version"], check=False)
if result.returncode != 0:
click.echo("Error: Docker is not running or not installed.", err=True)
click.echo("Please ensure Docker (or Rancher Desktop) is running.", err=True)
sys.exit(1)
except FileNotFoundError:
click.echo("Error: Docker command not found.", err=True)
click.echo("Please install Docker or Rancher Desktop.", err=True)
sys.exit(1)
def check_certificates():
"""Check if SSL certificates exist."""
required_files = ["ca.crt", "server.crt", "server.key"]
missing = [f for f in required_files if not (CERTS_DIR / f).exists()]
if missing:
click.echo("⚠️ Warning: Missing SSL certificate files:", err=True)
for f in missing:
click.echo(f" - {CERTS_DIR / f}", err=True)
click.echo("\nYou can:", err=True)
click.echo(" 1. Copy your dev-ca certificates to the certs/ directory", err=True)
click.echo(" 2. Generate self-signed certificates: ldap-docker certs generate", err=True)
return False
return True
@click.group()
@click.version_option(version="0.1.0")
def cli():
"""LDAP Docker Development Tool - Manage your OpenLDAP development environment."""
pass
@cli.group()
def server():
"""Manage the LDAP server container."""
pass
@server.command("start")
@click.option("--detach", "-d", is_flag=True, default=True, help="Run in detached mode")
@click.option("--build", is_flag=True, help="Build images before starting")
def server_start(detach: bool, build: bool):
"""Start the LDAP server and optional phpLDAPadmin."""
check_docker()
check_certificates()
cmd = ["docker-compose", "up"]
if detach:
cmd.append("-d")
if build:
cmd.append("--build")
click.echo("Starting LDAP server...")
result = run_command(cmd)
if result.returncode == 0:
click.echo("✅ LDAP server started successfully!")
click.echo(f"\nLDAP server is available at:")
click.echo(f" - LDAP: ldap://localhost:389")
click.echo(f" - LDAPS: ldaps://localhost:636")
click.echo(f" - Admin: http://localhost:8080 (phpLDAPadmin)")
click.echo(f"\nAdmin credentials:")
click.echo(f" - DN: {DEFAULT_ADMIN_DN}")
click.echo(f" - Password: {DEFAULT_ADMIN_PASSWORD}")
if detach:
click.echo("\nWaiting for server to be ready...")
time.sleep(5)
# Try to check if server is responding
result = run_command(
["docker-compose", "ps", "--filter", "status=running"],
check=False
)
if "ldap-server" in result.stdout:
click.echo("✅ Server is running")
@server.command("stop")
def server_stop():
"""Stop the LDAP server."""
check_docker()
click.echo("Stopping LDAP server...")
run_command(["docker-compose", "stop"])
click.echo("✅ LDAP server stopped")
@server.command("restart")
def server_restart():
"""Restart the LDAP server."""
check_docker()
click.echo("Restarting LDAP server...")
run_command(["docker-compose", "restart"])
click.echo("✅ LDAP server restarted")
click.echo("\nWaiting for server to be ready...")
time.sleep(5)
@server.command("down")
@click.option("--volumes", "-v", is_flag=True, help="Remove volumes (deletes all data)")
def server_down(volumes: bool):
"""Stop and remove the LDAP server containers."""
check_docker()
cmd = ["docker-compose", "down"]
if volumes:
if not click.confirm("⚠️ This will delete all LDAP data. Continue?"):
click.echo("Aborted.")
return
cmd.append("-v")
click.echo("Removing LDAP server containers...")
run_command(cmd)
click.echo("✅ Containers removed")
@server.command("logs")
@click.option("--follow", "-f", is_flag=True, help="Follow log output")
@click.option("--tail", "-n", default=100, help="Number of lines to show from the end")
@click.option("--service", default="openldap", help="Service to show logs for")
def server_logs(follow: bool, tail: int, service: str):
"""View LDAP server logs."""
check_docker()
cmd = ["docker-compose", "logs", f"--tail={tail}"]
if follow:
cmd.append("-f")
cmd.append(service)
# For follow mode, we want to pass through to the terminal
try:
subprocess.run(cmd, cwd=PROJECT_ROOT)
except KeyboardInterrupt:
click.echo("\n")
@server.command("status")
def server_status():
"""Check LDAP server status."""
check_docker()
result = run_command(["docker-compose", "ps"], check=False)
click.echo(result.stdout)
@cli.group()
def certs():
"""Manage SSL/TLS certificates."""
pass
@certs.command("generate")
@click.option("--force", is_flag=True, help="Overwrite existing certificates")
@click.option("--hostname", default="ldap.testing.local", help="Server hostname")
def certs_generate(force: bool, hostname: str):
"""Generate self-signed SSL certificates for development."""
script_path = PROJECT_ROOT / "scripts" / "generate_certs.py"
if not script_path.exists():
click.echo(f"Error: Certificate generation script not found: {script_path}", err=True)
sys.exit(1)
cmd = [sys.executable, str(script_path), "--hostname", hostname]
if force:
cmd.append("--force")
try:
subprocess.run(cmd, check=True)
except subprocess.CalledProcessError:
click.echo("Failed to generate certificates", err=True)
sys.exit(1)
@certs.command("check")
def certs_check():
"""Verify SSL certificates."""
required_files = {
"ca.crt": "CA Certificate",
"server.crt": "Server Certificate",
"server.key": "Server Private Key",
}
click.echo("Checking SSL certificates...\n")
all_exist = True
for filename, description in required_files.items():
filepath = CERTS_DIR / filename
if filepath.exists():
size = filepath.stat().st_size
click.echo(f"{description}: {filepath} ({size} bytes)")
else:
click.echo(f"{description}: {filepath} (missing)")
all_exist = False
if all_exist:
click.echo("\n✅ All required certificates are present")
# Try to verify the certificate chain
try:
result = run_command([
"openssl", "verify", "-CAfile",
str(CERTS_DIR / "ca.crt"),
str(CERTS_DIR / "server.crt")
], check=False)
if result.returncode == 0:
click.echo("✅ Certificate chain is valid")
else:
click.echo("⚠️ Certificate chain verification failed")
click.echo(result.stderr)
except FileNotFoundError:
click.echo(" OpenSSL not found, skipping certificate verification")
else:
click.echo("\n❌ Some certificates are missing")
click.echo("Run 'ldap-docker certs generate' to create them")
sys.exit(1)
@cli.group()
def test():
"""Test LDAP server connectivity and queries."""
pass
@test.command("connection")
@click.option("--host", default=DEFAULT_HOST, help="LDAP server host")
@click.option("--port", default=DEFAULT_PORT, help="LDAP server port")
@click.option("--use-ssl", is_flag=True, help="Use LDAPS instead of LDAP")
def test_connection(host: str, port: int, use_ssl: bool):
"""Test basic connection to LDAP server."""
if ldap3 is None:
click.echo("Error: ldap3 library not installed", err=True)
click.echo("Install it with: uv pip install ldap3", err=True)
sys.exit(1)
if use_ssl:
port = DEFAULT_LDAPS_PORT
url = f"ldaps://{host}:{port}"
else:
url = f"ldap://{host}:{port}"
click.echo(f"Testing connection to {url}...")
try:
server = Server(url, get_info=ALL, use_ssl=use_ssl)
conn = Connection(server, auto_bind=True)
click.echo("✅ Successfully connected to LDAP server")
click.echo(f"\nServer info:")
click.echo(f" Vendor: {server.info.vendor_name if server.info else 'Unknown'}")
click.echo(f" Version: {server.info.vendor_version if server.info else 'Unknown'}")
conn.unbind()
except LDAPException as e:
click.echo(f"❌ Connection failed: {e}", err=True)
sys.exit(1)
@test.command("auth")
@click.option("--host", default=DEFAULT_HOST, help="LDAP server host")
@click.option("--port", default=DEFAULT_PORT, help="LDAP server port")
@click.option("--use-ssl", is_flag=True, help="Use LDAPS")
@click.option("--user", default=DEFAULT_ADMIN_DN, help="User DN")
@click.option("--password", default=DEFAULT_ADMIN_PASSWORD, help="Password")
def test_auth(host: str, port: int, use_ssl: bool, user: str, password: str):
"""Test authentication with LDAP server."""
if ldap3 is None:
click.echo("Error: ldap3 library not installed", err=True)
click.echo("Install it with: uv pip install ldap3", err=True)
sys.exit(1)
if use_ssl:
port = DEFAULT_LDAPS_PORT
url = f"ldaps://{host}:{port}"
else:
url = f"ldap://{host}:{port}"
click.echo(f"Testing authentication to {url}...")
click.echo(f"User: {user}")
try:
server = Server(url, get_info=ALL, use_ssl=use_ssl)
conn = Connection(server, user=user, password=password, auto_bind=True)
click.echo("✅ Authentication successful")
# Try to perform a simple search
conn.search(DEFAULT_BASE_DN, "(objectClass=*)", search_scope="BASE")
if conn.entries:
click.echo(f"✅ Base DN accessible: {DEFAULT_BASE_DN}")
conn.unbind()
except LDAPException as e:
click.echo(f"❌ Authentication failed: {e}", err=True)
sys.exit(1)
@test.command("users")
@click.option("--host", default=DEFAULT_HOST, help="LDAP server host")
@click.option("--port", default=DEFAULT_PORT, help="LDAP server port")
@click.option("--use-ssl", is_flag=True, help="Use LDAPS")
def test_users(host: str, port: int, use_ssl: bool):
"""List all users in the LDAP directory."""
if ldap3 is None:
click.echo("Error: ldap3 library not installed", err=True)
click.echo("Install it with: uv pip install ldap3", err=True)
sys.exit(1)
if use_ssl:
port = DEFAULT_LDAPS_PORT
url = f"ldaps://{host}:{port}"
else:
url = f"ldap://{host}:{port}"
try:
server = Server(url, get_info=ALL, use_ssl=use_ssl)
conn = Connection(
server,
user=DEFAULT_ADMIN_DN,
password=DEFAULT_ADMIN_PASSWORD,
auto_bind=True
)
# Search for all users
conn.search(
DEFAULT_BASE_DN,
"(objectClass=inetOrgPerson)",
attributes=["uid", "cn", "mail", "uidNumber"]
)
if conn.entries:
click.echo(f"Found {len(conn.entries)} user(s):\n")
for entry in conn.entries:
click.echo(f" - {entry.cn}: {entry.uid} ({entry.mail})")
else:
click.echo("No users found")
conn.unbind()
except LDAPException as e:
click.echo(f"❌ Query failed: {e}", err=True)
sys.exit(1)
@cli.command("init")
def init():
"""Initialize the LDAP Docker environment."""
click.echo("Initializing LDAP Docker environment...\n")
# Check Docker
click.echo("1. Checking Docker...")
check_docker()
click.echo(" ✅ Docker is available\n")
# Check certificates
click.echo("2. Checking SSL certificates...")
if not check_certificates():
if click.confirm("\nGenerate self-signed certificates now?", default=True):
certs_generate.callback(force=False, hostname="ldap.testing.local")
else:
click.echo("\n You can generate certificates later with: ldap-docker certs generate")
click.echo(" Or copy your dev-ca certificates to the certs/ directory")
else:
click.echo(" ✅ Certificates are present\n")
# Start server
click.echo("\n3. Starting LDAP server...")
if click.confirm("Start the LDAP server now?", default=True):
server_start.callback(detach=True, build=False)
else:
click.echo("\n You can start the server later with: ldap-docker server start")
click.echo("\n✅ Initialization complete!")
click.echo("\nUseful commands:")
click.echo(" - View logs: ldap-docker server logs -f")
click.echo(" - Test connection: ldap-docker test connection")
click.echo(" - List users: ldap-docker test users")
click.echo(" - Stop server: ldap-docker server stop")
def main():
"""Entry point for the CLI."""
try:
cli()
except KeyboardInterrupt:
click.echo("\n\nInterrupted by user")
sys.exit(130)
except Exception as e:
click.echo(f"Error: {e}", err=True)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -9,7 +9,7 @@ For production, use proper certificates from your dev-ca or a trusted CA.
import argparse
import ipaddress
import sys
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from pathlib import Path
try:
@@ -17,7 +17,7 @@ try:
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import ExtensionOID, NameOID
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
except ImportError:
print("Error: cryptography library not found.")
print("Install it with: uv pip install cryptography")
@@ -55,8 +55,8 @@ def generate_ca_certificate(
.issuer_name(issuer)
.public_key(private_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.utcnow())
.not_valid_after(datetime.utcnow() + timedelta(days=days_valid))
.not_valid_before(datetime.now(timezone.utc))
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=days_valid))
.add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True,
@@ -90,7 +90,7 @@ def generate_server_certificate(
ca_cert: x509.Certificate,
ca_key: rsa.RSAPrivateKey,
hostname: str = "ldap.testing.local",
san_list: list[str] = None,
san_list: list[str] | None = None,
days_valid: int = 365,
) -> x509.Certificate:
"""Generate a server certificate signed by the CA."""
@@ -123,8 +123,8 @@ def generate_server_certificate(
.issuer_name(ca_cert.subject)
.public_key(private_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(datetime.utcnow())
.not_valid_after(datetime.utcnow() + timedelta(days=days_valid))
.not_valid_before(datetime.now(timezone.utc))
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=days_valid))
.add_extension(
x509.SubjectAlternativeName(san_entries),
critical=False,
@@ -148,7 +148,7 @@ def generate_server_certificate(
critical=True,
)
.add_extension(
x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]),
x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]),
critical=False,
)
.add_extension(
@@ -236,9 +236,7 @@ def main():
# Check if certificates already exist
if not args.force:
existing = [
p for p in [ca_cert_path, server_cert_path, server_key_path] if p.exists()
]
existing = [p for p in [ca_cert_path, server_cert_path, server_key_path] if p.exists()]
if existing:
print("Error: The following certificate files already exist:")
for p in existing:

View File

@@ -1,195 +0,0 @@
#!/usr/bin/env bash
#
# LDAP Search Helper
# Generates ldapsearch commands configured for your environment
#
set -e
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Get the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
# Load environment variables from .env if it exists
if [ -f "$PROJECT_ROOT/.env" ]; then
source "$PROJECT_ROOT/.env"
fi
# Set defaults if not in environment
LDAP_PORT=${LDAP_PORT:-389}
LDAPS_PORT=${LDAPS_PORT:-636}
LDAP_BASE_DN=${LDAP_BASE_DN:-dc=testing,dc=local}
LDAP_ADMIN_PASSWORD=${LDAP_ADMIN_PASSWORD:-admin_password}
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}LDAP Search Command Generator${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
echo "Configuration:"
echo " LDAP Port: $LDAP_PORT"
echo " LDAPS Port: $LDAPS_PORT"
echo " Base DN: $LDAP_BASE_DN"
echo ""
# Function to print a command example
print_cmd() {
local description="$1"
local command="$2"
echo -e "${GREEN}# $description${NC}"
echo -e "${YELLOW}$command${NC}"
echo ""
}
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Basic Connection Tests${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
print_cmd "Test server is responding (anonymous bind)" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -x -b \"\" -s base"
print_cmd "Test with admin credentials" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"$LDAP_BASE_DN\""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Search Users${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
print_cmd "List all users" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=people,$LDAP_BASE_DN\" \"(objectClass=inetOrgPerson)\" uid cn mail"
print_cmd "Search for specific user (jdoe)" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"$LDAP_BASE_DN\" \"(uid=jdoe)\""
print_cmd "Test user authentication (as jdoe)" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"uid=jdoe,ou=people,$LDAP_BASE_DN\" -w password123 -b \"$LDAP_BASE_DN\" \"(uid=jdoe)\""
print_cmd "Get all user attributes for jdoe" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=people,$LDAP_BASE_DN\" \"(uid=jdoe)\" '*' '+'"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Search Groups${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
print_cmd "List all groups" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=groups,$LDAP_BASE_DN\" \"(objectClass=groupOfNames)\" cn member"
print_cmd "Find groups for user jdoe" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=groups,$LDAP_BASE_DN\" \"(member=uid=jdoe,ou=people,$LDAP_BASE_DN)\" cn"
print_cmd "Get members of developers group" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=groups,$LDAP_BASE_DN\" \"(cn=developers)\" member"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}LDAPS (SSL/TLS) Commands${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
print_cmd "Test LDAPS connection" \
"ldapsearch -H ldaps://localhost:$LDAPS_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"$LDAP_BASE_DN\""
print_cmd "LDAPS with CA certificate verification" \
"LDAPTLS_CACERT=$PROJECT_ROOT/certs/ca.crt ldapsearch -H ldaps://localhost:$LDAPS_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"$LDAP_BASE_DN\""
print_cmd "Check SSL certificate" \
"openssl s_client -connect localhost:$LDAPS_PORT -CAfile $PROJECT_ROOT/certs/ca.crt -showcerts"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Secure Commands (Password Prompt)${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
print_cmd "Admin search with password prompt (more secure)" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -W -b \"$LDAP_BASE_DN\""
print_cmd "User authentication with password prompt" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"uid=jdoe,ou=people,$LDAP_BASE_DN\" -W -b \"$LDAP_BASE_DN\" \"(uid=jdoe)\""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Advanced Queries${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
print_cmd "Search with wildcard (all users starting with 'j')" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=people,$LDAP_BASE_DN\" \"(uid=j*)\" uid cn"
print_cmd "Search by email domain" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=people,$LDAP_BASE_DN\" \"(mail=*@testing.local)\" uid mail"
print_cmd "Count total users" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=people,$LDAP_BASE_DN\" \"(objectClass=inetOrgPerson)\" dn | grep -c '^dn:'"
print_cmd "Export entire directory to LDIF file" \
"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"$LDAP_BASE_DN\" -LLL > ldap_backup.ldif"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Quick Reference${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
echo "Common flags:"
echo " -H : LDAP URI (ldap:// or ldaps://)"
echo " -D : Bind DN (user to authenticate as)"
echo " -w : Password (visible in process list)"
echo " -W : Prompt for password (more secure)"
echo " -x : Use simple authentication"
echo " -b : Base DN to search from"
echo " -s : Scope (base, one, sub)"
echo " -LLL: LDIF output without comments"
echo ""
echo "Test credentials:"
echo " Admin: cn=admin,$LDAP_BASE_DN / $LDAP_ADMIN_PASSWORD"
echo " Test user: uid=jdoe,ou=people,$LDAP_BASE_DN / password123"
echo ""
echo -e "${GREEN}Tip:${NC} Copy any command above and run it in your terminal!"
echo ""
# Option to run a command interactively
if [ "$1" = "--interactive" ] || [ "$1" = "-i" ]; then
echo ""
echo "Select a command to run:"
echo " 1) List all users"
echo " 2) Search for user jdoe"
echo " 3) Test user authentication"
echo " 4) List all groups"
echo " 5) Test LDAPS connection"
echo " 6) Custom command"
echo ""
read -p "Choice (1-6): " choice
case $choice in
1)
ldapsearch -H ldap://localhost:$LDAP_PORT -D "cn=admin,$LDAP_BASE_DN" -w $LDAP_ADMIN_PASSWORD -b "ou=people,$LDAP_BASE_DN" "(objectClass=inetOrgPerson)" uid cn mail
;;
2)
ldapsearch -H ldap://localhost:$LDAP_PORT -D "cn=admin,$LDAP_BASE_DN" -w $LDAP_ADMIN_PASSWORD -b "$LDAP_BASE_DN" "(uid=jdoe)"
;;
3)
read -p "Enter password for jdoe: " -s user_pass
echo ""
ldapsearch -H ldap://localhost:$LDAP_PORT -D "uid=jdoe,ou=people,$LDAP_BASE_DN" -w "$user_pass" -b "$LDAP_BASE_DN" "(uid=jdoe)"
;;
4)
ldapsearch -H ldap://localhost:$LDAP_PORT -D "cn=admin,$LDAP_BASE_DN" -w $LDAP_ADMIN_PASSWORD -b "ou=groups,$LDAP_BASE_DN" "(objectClass=groupOfNames)" cn member
;;
5)
LDAPTLS_CACERT=$PROJECT_ROOT/certs/ca.crt ldapsearch -H ldaps://localhost:$LDAPS_PORT -D "cn=admin,$LDAP_BASE_DN" -w $LDAP_ADMIN_PASSWORD -b "$LDAP_BASE_DN"
;;
6)
read -p "Enter custom filter: " filter
ldapsearch -H ldap://localhost:$LDAP_PORT -D "cn=admin,$LDAP_BASE_DN" -w $LDAP_ADMIN_PASSWORD -b "$LDAP_BASE_DN" "$filter"
;;
*)
echo "Invalid choice"
exit 1
;;
esac
fi

View File

@@ -5,8 +5,9 @@ This module contains unit tests for the generate_certs.py script.
"""
import tempfile
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import TYPE_CHECKING, cast
import pytest
@@ -14,18 +15,25 @@ try:
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from cryptography.x509 import BasicConstraints, SubjectAlternativeName
from cryptography.x509.oid import ExtensionOID, NameOID
CRYPTO_AVAILABLE = True
except ImportError:
CRYPTO_AVAILABLE = False
if TYPE_CHECKING:
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509 import BasicConstraints, SubjectAlternativeName
from cryptography.x509.oid import ExtensionOID, NameOID
# Import the module to test
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
if CRYPTO_AVAILABLE:
if CRYPTO_AVAILABLE or TYPE_CHECKING:
from scripts.generate_certs import (
generate_ca_certificate,
generate_private_key,
@@ -63,10 +71,8 @@ class TestCertificateGeneration:
assert cert.subject == cert.issuer
# Check basic constraints
basic_constraints = cert.extensions.get_extension_for_oid(
x509.ExtensionOID.BASIC_CONSTRAINTS
)
assert basic_constraints.value.ca is True
basic_constraints = cert.extensions.get_extension_for_oid(ExtensionOID.BASIC_CONSTRAINTS)
assert cast(BasicConstraints, basic_constraints.value).ca is True
def test_generate_ca_certificate_validity(self):
"""Test that CA certificate has correct validity period."""
@@ -74,11 +80,11 @@ class TestCertificateGeneration:
days_valid = 365
cert = generate_ca_certificate(key, days_valid=days_valid)
now = datetime.utcnow()
now = datetime.now(timezone.utc)
expected_expiry = now + timedelta(days=days_valid)
# Allow 1 minute tolerance for test execution time
assert abs((cert.not_valid_after - expected_expiry).total_seconds()) < 60
assert abs((cert.not_valid_after_utc - expected_expiry).total_seconds()) < 60
def test_generate_server_certificate(self):
"""Test that a server certificate can be generated."""
@@ -105,9 +111,9 @@ class TestCertificateGeneration:
# Check basic constraints - should not be a CA
basic_constraints = server_cert.extensions.get_extension_for_oid(
x509.ExtensionOID.BASIC_CONSTRAINTS
ExtensionOID.BASIC_CONSTRAINTS
)
assert basic_constraints.value.ca is False
assert cast(BasicConstraints, basic_constraints.value).ca is False
def test_generate_server_certificate_san(self):
"""Test that server certificate includes Subject Alternative Names."""
@@ -126,11 +132,15 @@ class TestCertificateGeneration:
# Get SAN extension
san_ext = server_cert.extensions.get_extension_for_oid(
x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
# Extract DNS names
dns_names = [name.value for name in san_ext.value if isinstance(name, x509.DNSName)]
dns_names = [
name.value
for name in cast(SubjectAlternativeName, san_ext.value)
if isinstance(name, x509.DNSName)
]
# Check that all DNS names are present
for name in san_list:
@@ -191,15 +201,15 @@ class TestCertificateGeneration:
# Verify server cert is not a CA
server_basic_constraints = server_cert.extensions.get_extension_for_oid(
x509.ExtensionOID.BASIC_CONSTRAINTS
ExtensionOID.BASIC_CONSTRAINTS
)
assert server_basic_constraints.value.ca is False
assert cast(BasicConstraints, server_basic_constraints.value).ca is False
# Verify CA cert is a CA
ca_basic_constraints = ca_cert.extensions.get_extension_for_oid(
x509.ExtensionOID.BASIC_CONSTRAINTS
ExtensionOID.BASIC_CONSTRAINTS
)
assert ca_basic_constraints.value.ca is True
assert cast(BasicConstraints, ca_basic_constraints.value).ca is True
@pytest.mark.skipif(not CRYPTO_AVAILABLE, reason="cryptography library not available")
@@ -219,12 +229,12 @@ class TestCertificateValidation:
# Check server certificate extensions
ext_oids = [ext.oid for ext in server_cert.extensions]
assert x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME in ext_oids
assert x509.ExtensionOID.BASIC_CONSTRAINTS in ext_oids
assert x509.ExtensionOID.KEY_USAGE in ext_oids
assert x509.ExtensionOID.EXTENDED_KEY_USAGE in ext_oids
assert x509.ExtensionOID.SUBJECT_KEY_IDENTIFIER in ext_oids
assert x509.ExtensionOID.AUTHORITY_KEY_IDENTIFIER in ext_oids
assert ExtensionOID.SUBJECT_ALTERNATIVE_NAME in ext_oids
assert ExtensionOID.BASIC_CONSTRAINTS in ext_oids
assert ExtensionOID.KEY_USAGE in ext_oids
assert ExtensionOID.EXTENDED_KEY_USAGE in ext_oids
assert ExtensionOID.SUBJECT_KEY_IDENTIFIER in ext_oids
assert ExtensionOID.AUTHORITY_KEY_IDENTIFIER in ext_oids
def test_certificate_validity_dates(self):
"""Test that certificates have correct validity dates."""
@@ -232,14 +242,14 @@ class TestCertificateValidation:
days_valid = 100
cert = generate_ca_certificate(key, days_valid=days_valid)
now = datetime.utcnow()
now = datetime.now(timezone.utc)
# Check not_valid_before is around now
assert abs((cert.not_valid_before - now).total_seconds()) < 60
assert abs((cert.not_valid_before_utc - now).total_seconds()) < 60
# Check not_valid_after is around now + days_valid
expected_expiry = now + timedelta(days=days_valid)
assert abs((cert.not_valid_after - expected_expiry).total_seconds()) < 60
assert abs((cert.not_valid_after_utc - expected_expiry).total_seconds()) < 60
def test_imports():