First commit from the robot

This commit is contained in:
2025-10-16 14:57:56 -07:00
committed by Spencer Jones
parent c747ae4ce3
commit 0bac69c801
18 changed files with 3952 additions and 0 deletions

102
.env.example Normal file
View File

@@ -0,0 +1,102 @@
# LDAP Docker Environment Configuration
# Copy this file to .env and customize as needed
# Note: .env is git-ignored to prevent committing secrets
# ============================================================================
# LDAP Domain Configuration
# ============================================================================
# The LDAP domain (e.g., testing.local -> dc=testing,dc=local)
LDAP_DOMAIN=testing.local
# Organization name
LDAP_ORGANISATION=Testing Organization
# Base DN (automatically derived from LDAP_DOMAIN if not set)
LDAP_BASE_DN=dc=testing,dc=local
# ============================================================================
# Admin Credentials
# ============================================================================
# LDAP admin password
# WARNING: Change this for any environment accessible by others
LDAP_ADMIN_PASSWORD=admin_password
# LDAP config password (for cn=config)
LDAP_CONFIG_PASSWORD=config_password
# ============================================================================
# SSL/TLS Configuration
# ============================================================================
# Enable TLS/SSL
LDAP_TLS=true
# Certificate filenames (relative to certs/ directory)
LDAP_TLS_CRT_FILENAME=server.crt
LDAP_TLS_KEY_FILENAME=server.key
LDAP_TLS_CA_CRT_FILENAME=ca.crt
# TLS verification level: never, allow, try, demand
LDAP_TLS_VERIFY_CLIENT=try
# ============================================================================
# Port Configuration
# ============================================================================
# Standard LDAP port (unencrypted)
LDAP_PORT=389
# LDAPS port (SSL/TLS)
LDAPS_PORT=636
# phpLDAPadmin web interface port
PHPLDAPADMIN_PORT=8080
# ============================================================================
# Logging Configuration
# ============================================================================
# LDAP log level
# 0 = no logging, 256 = stats logging, -1 = any logging
LDAP_LOG_LEVEL=256
# ============================================================================
# Container Configuration
# ============================================================================
# Hostname for the LDAP server
LDAP_HOSTNAME=ldap.testing.local
# Container name
LDAP_CONTAINER_NAME=ldap-server
# phpLDAPadmin container name
PHPLDAPADMIN_CONTAINER_NAME=ldap-admin
# ============================================================================
# Optional: Replication Configuration (Advanced)
# ============================================================================
# Enable replication (leave commented for single-server setup)
# LDAP_REPLICATION=true
# LDAP_REPLICATION_CONFIG_SYNCPROV=binddn="cn=admin,cn=config" bindmethod=simple credentials=$LDAP_CONFIG_PASSWORD searchbase="cn=config" type=refreshAndPersist retry="60 +" timeout=1 starttls=critical
# LDAP_REPLICATION_DB_SYNCPROV=binddn="cn=admin,$LDAP_BASE_DN" bindmethod=simple credentials=$LDAP_ADMIN_PASSWORD searchbase="$LDAP_BASE_DN" type=refreshAndPersist interval=00:00:00:10 retry="60 +" timeout=1 starttls=critical
# ============================================================================
# Optional: Backup Configuration
# ============================================================================
# Backup directory (uncomment to enable)
# BACKUP_DIR=./backups
# ============================================================================
# Development Settings
# ============================================================================
# Set to "true" to enable debug output
DEBUG=false
# Timezone (optional)
TZ=UTC

117
.gitignore vendored Normal file
View File

@@ -0,0 +1,117 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
.python-version
# UV specific
.uv/
uv.lock
# Testing
.pytest_cache/
.coverage
.coverage.*
htmlcov/
.tox/
.nox/
coverage.xml
*.cover
.hypothesis/
# Type checking
.mypy_cache/
.dmypy.json
dmypy.json
.pytype/
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Certificates - DO NOT COMMIT PRIVATE KEYS
certs/*.key
certs/*.crt
certs/*.pem
certs/*.csr
certs/ca.key
certs/ca.crt
certs/server.key
certs/server.crt
# Keep the README
!certs/README.md
# Docker volumes and data
docker-compose.override.yml
.docker/
# Logs
*.log
logs/
# Backup files
*.bak
*.tmp
# Environment files with secrets
.env.local
.env.*.local
# MacOS
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
# Thumbnails
.Thumbs.db
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

288
GETTING_STARTED.md Normal file
View File

@@ -0,0 +1,288 @@
# 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`.

187
Makefile Normal file
View File

@@ -0,0 +1,187 @@
.PHONY: help install init start stop restart down logs status certs-generate certs-check test-connection test-auth test-users clean clean-all
# Default target
.DEFAULT_GOAL := help
help: ## Show this help message
@echo "LDAP Docker Development Tool"
@echo ""
@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}'
install: ## Install Python dependencies with UV
@echo "Installing dependencies with UV..."
@command -v uv >/dev/null 2>&1 || { echo "Error: uv not found. Install it with: curl -LsSf https://astral.sh/uv/install.sh | sh"; exit 1; }
uv sync
@echo "✅ Dependencies installed"
install-dev: ## Install development dependencies
@echo "Installing development dependencies with UV..."
uv sync --all-extras
@echo "✅ Development dependencies installed"
init: install certs-check ## Initialize the environment (install deps, check certs)
@echo ""
@echo "Initialization complete!"
@echo "Run 'make start' to start the LDAP server"
certs-generate: ## Generate self-signed SSL certificates
@echo "Generating SSL certificates..."
uv run python scripts/generate_certs.py
@echo "✅ Certificates generated"
certs-check: ## Check if SSL certificates exist
@echo "Checking SSL certificates..."
@if [ ! -f certs/ca.crt ] || [ ! -f certs/server.crt ] || [ ! -f certs/server.key ]; then \
echo "⚠️ Warning: SSL certificates not found"; \
echo ""; \
echo "You can:"; \
echo " 1. Copy your dev-ca certificates to certs/"; \
echo " cp /path/to/dev-ca/ca.crt certs/"; \
echo " cp /path/to/dev-ca/server.crt certs/"; \
echo " cp /path/to/dev-ca/server.key certs/"; \
echo " 2. Generate self-signed certs: make certs-generate"; \
echo ""; \
exit 1; \
else \
echo "✅ SSL certificates found"; \
fi
start: ## Start the LDAP server
@echo "Starting LDAP server..."
docker-compose up -d
@echo "✅ LDAP server started"
@echo ""
@echo "Services available at:"
@echo " - LDAP: ldap://localhost:389"
@echo " - LDAPS: ldaps://localhost:636"
@echo " - Admin: http://localhost:8080"
@echo ""
@echo "Admin credentials:"
@echo " DN: cn=admin,dc=testing,dc=local"
@echo " Password: admin_password"
@echo ""
@echo "Run 'make logs' to view logs"
stop: ## Stop the LDAP server
@echo "Stopping LDAP server..."
docker-compose stop
@echo "✅ LDAP server stopped"
restart: ## Restart the LDAP server
@echo "Restarting LDAP server..."
docker-compose restart
@echo "✅ LDAP server restarted"
down: ## Stop and remove containers (keeps data)
@echo "Stopping and removing containers..."
docker-compose down
@echo "✅ Containers removed (data preserved)"
down-volumes: ## Stop and remove containers AND volumes (deletes all data)
@echo "⚠️ WARNING: This will delete all LDAP data!"
@read -p "Are you sure? [y/N] " -n 1 -r; \
echo; \
if [[ $$REPLY =~ ^[Yy]$$ ]]; then \
docker-compose down -v; \
echo "✅ Containers and volumes removed"; \
else \
echo "Aborted"; \
fi
logs: ## View LDAP server logs (follow mode)
docker-compose logs -f openldap
logs-tail: ## View last 100 lines of logs
docker-compose logs --tail=100 openldap
logs-admin: ## View phpLDAPadmin logs
docker-compose logs -f phpldapadmin
status: ## Show container status
@docker-compose ps
test-connection: ## Test connection to LDAP server
@echo "Testing LDAP connection..."
uv run python -c "from ldap3 import Server, Connection, ALL; s = Server('ldap://localhost: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..."
uv run python -c "from ldap3 import Server, Connection; s = Server('ldap://localhost: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
@echo "Listing LDAP users..."
@uv run python -c "from ldap3 import Server, Connection; s = Server('ldap://localhost: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..."
openssl s_client -connect localhost:636 -CAfile certs/ca.crt </dev/null
test-all: test-connection test-auth test-users ## Run all tests
shell: ## Open a shell in the LDAP container
docker-compose exec openldap bash
ldapsearch: ## Run ldapsearch command (example query)
@echo "Running ldapsearch..."
ldapsearch -H ldap://localhost:389 -x -b "dc=testing,dc=local" -D "cn=admin,dc=testing,dc=local" -w admin_password
clean: ## Clean Python build artifacts
@echo "Cleaning build artifacts..."
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
find . -type f -name "*.pyc" -delete 2>/dev/null || true
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/
@echo "✅ Cleaned"
clean-all: clean down-volumes ## Clean everything including Docker volumes
@echo "Cleaning certificates (keeping README)..."
find certs/ -type f ! -name "README.md" -delete 2>/dev/null || true
@echo "✅ Full cleanup complete"
dev-setup: install-dev certs-generate start ## Complete development setup
@echo ""
@echo "🎉 Development environment ready!"
@echo ""
@echo "Next steps:"
@echo " - View logs: make logs"
@echo " - Test connection: make test-connection"
@echo " - List users: make test-users"
@echo " - Open admin UI: open http://localhost:8080"
quick-start: certs-check start ## Quick start (assumes certs exist)
@echo "🚀 LDAP server is running!"
rebuild: down ## Rebuild and restart containers
@echo "Rebuilding containers..."
docker-compose up -d --build
@echo "✅ Containers rebuilt and started"
# UV-specific targets
uv-install: ## Install UV package manager
@echo "Installing UV..."
@command -v uv >/dev/null 2>&1 && echo "✅ UV already installed" || curl -LsSf https://astral.sh/uv/install.sh | sh
uv-sync: ## Sync dependencies with UV
uv sync
uv-update: ## Update all dependencies
uv lock --upgrade
uv sync
# Docker checks
check-docker: ## Check if Docker is running
@docker version >/dev/null 2>&1 && echo "✅ Docker is running" || (echo "❌ Docker is not running. Please start Docker or Rancher Desktop." && exit 1)
check-compose: ## Check if docker-compose is available
@docker-compose version >/dev/null 2>&1 && echo "✅ docker-compose is available" || (echo "❌ docker-compose not found" && exit 1)
check-all: check-docker check-compose certs-check ## Run all checks
@echo "✅ All checks passed"

354
QUICKREF.md Normal file
View File

@@ -0,0 +1,354 @@
# 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

459
README.md Normal file
View File

@@ -0,0 +1,459 @@
# 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.
Perfect for local development and testing of applications that need LDAP authentication without setting up a complex infrastructure.
## 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
## Table of Contents
- [Prerequisites](#prerequisites)
- [Quick Start](#quick-start)
- [Custom Certificates (Dev-CA)](#custom-certificates-dev-ca)
- [Usage](#usage)
- [Test Users](#test-users)
- [Management Tools](#management-tools)
- [Project Structure](#project-structure)
- [Troubleshooting](#troubleshooting)
- [Development](#development)
## Prerequisites
### 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)
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
### Optional
- **OpenSSL** (for certificate verification)
- **LDAP utilities** (ldapsearch, ldapadd, etc.) for testing
## Quick Start
### Option 1: Using Make (Recommended)
```bash
# 1. Install dependencies
make install
# 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
docker-compose up -d
# 3. View logs
docker-compose logs -f openldap
```
### Option 3: Complete Dev Setup
This will install everything, generate certificates, and start the server:
```bash
make dev-setup
```
## Custom Certificates (Dev-CA)
If you maintain your own dev-ca (as mentioned), you can use your own certificates instead of self-signed ones:
### Using Your Dev-CA Certificates
```bash
# Copy your certificates to the certs directory
cp /path/to/your/dev-ca/certs/ldap-server.crt certs/server.crt
cp /path/to/your/dev-ca/private/ldap-server.key certs/server.key
cp /path/to/your/dev-ca/ca-cert.pem certs/ca.crt
# Ensure proper permissions
chmod 644 certs/ca.crt
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).
### 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
### Generating Certificates with Your OpenSSL-based Dev-CA
If your dev-ca is script-based and uses OpenSSL:
```bash
# Example using your dev-ca
cd /path/to/your/dev-ca
# Generate server key
openssl genrsa -out ldap-server.key 4096
# Generate certificate signing request
openssl req -new -key ldap-server.key -out ldap-server.csr \
-subj "/CN=ldap.testing.local"
# 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")
# Copy to LDAP Docker project
cp ldap-server.crt /path/to/ldap_docker/certs/server.crt
cp ldap-server.key /path/to/ldap_docker/certs/server.key
cp ca-cert.pem /path/to/ldap_docker/certs/ca.crt
```
## Usage
### Accessing the Services
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 |
### Admin Credentials
- **Admin DN:** `cn=admin,dc=testing,dc=local`
- **Password:** `admin_password`
- **Base DN:** `dc=testing,dc=local`
### Common Commands
```bash
# Start the server
make start
# Stop the server
make stop
# View logs
make logs
# Check status
make status
# Run tests
make test-all
# Open shell in container
make shell
# Clean up everything
make clean-all
```
## 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 |
### Test Groups
- **admins** - Administrator group (member: admin)
- **developers** - Development team (members: jdoe, jsmith)
- **users** - General users (members: jdoe, jsmith, testuser)
### Testing Authentication
```bash
# Test with ldapsearch
ldapsearch -H ldap://localhost:389 \
-D "uid=jdoe,ou=people,dc=testing,dc=local" \
-w password123 \
-b "dc=testing,dc=local" \
"(uid=jdoe)"
# Test LDAPS with SSL
ldapsearch -H ldaps://localhost:636 \
-D "uid=jdoe,ou=people,dc=testing,dc=local" \
-w password123 \
-b "dc=testing,dc=local" \
"(uid=jdoe)"
```
## Management Tools
This project includes Python-based management tools using UV.
### Installation
```bash
# Install dependencies with UV
make install
# Or manually
uv sync
```
### CLI Tool Usage
```bash
# View available commands
uv run ldap-docker --help
# Server management
uv run ldap-docker server start
uv run ldap-docker server stop
uv run ldap-docker server logs -f
# Certificate management
uv run ldap-docker certs generate
uv run ldap-docker certs check
# Testing
uv run ldap-docker test connection
uv run ldap-docker test auth
uv run ldap-docker test users
# Initialize environment
uv run ldap-docker init
```
## Project Structure
```
ldap_docker/
├── certs/ # SSL/TLS certificates (git-ignored)
│ ├── README.md # Certificate documentation
│ ├── ca.crt # CA certificate (your dev-ca)
│ ├── server.crt # Server certificate
│ └── server.key # Server private key
├── 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)
├── Makefile # Convenient command shortcuts
├── .gitignore # Git ignore rules
└── README.md # This file
```
## Troubleshooting
### Docker/Rancher Desktop Issues
**Problem:** `docker` command not found
```bash
# For Rancher Desktop on MacOS, ensure it's running and configured
# Check Docker settings in Rancher Desktop preferences
```
**Problem:** Cannot connect to Docker daemon
```bash
# Ensure Rancher Desktop or Docker Desktop is running
# On MacOS: Check if Rancher Desktop is in the menu bar
```
### Certificate Issues
**Problem:** LDAPS connection fails with certificate error
```bash
# Verify certificates exist
make certs-check
# Check certificate details
openssl x509 -in certs/server.crt -text -noout
# Verify certificate chain
openssl verify -CAfile certs/ca.crt certs/server.crt
```
**Problem:** Wrong hostname in certificate
```bash
# Regenerate with correct hostname
make certs-generate
# Or copy certificates from your dev-ca with correct hostname
```
### Connection Issues
**Problem:** Cannot connect to LDAP server
```bash
# Check if containers are running
docker-compose ps
# View logs for errors
make logs
# Test basic connectivity
telnet localhost 389
```
**Problem:** Authentication fails
```bash
# Verify credentials
# Default admin: cn=admin,dc=testing,dc=local / admin_password
# Check LDAP logs
docker-compose logs openldap | grep -i error
```
### Data Issues
**Problem:** Users not appearing
```bash
# Check if LDIF files were loaded
docker-compose logs openldap | grep -i ldif
# Restart and reload
make down-volumes # WARNING: Deletes data
make start
```
## Development
### Adding Custom Users
Edit `ldif/01-users.ldif` to add more users or modify existing ones:
```ldif
dn: uid=newuser,ou=people,dc=testing,dc=local
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: newuser
cn: New User
sn: User
mail: newuser@testing.local
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ=
uidNumber: 10004
gidNumber: 10004
homeDirectory: /home/newuser
loginShell: /bin/bash
```
Then restart with fresh data:
```bash
make down-volumes
make start
```
### Modifying Configuration
Edit `docker-compose.yml` to change:
- Port mappings
- Environment variables
- Volume mounts
- Resource limits
### Python Development
```bash
# Install development dependencies
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/
```
### Building on Other Platforms
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)
The `docker-compose.yml` uses standard Docker images and should work anywhere Docker runs.
## Security Notes
⚠️ **This is a development tool only!**
- Default passwords are weak and well-known
- Self-signed certificates are not trusted
- No backup or disaster recovery
- No monitoring or alerting
- Not hardened for production use
**Never use this in production or with real user data!**
## License
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
4. Test with both Docker Desktop and Rancher Desktop
## Resources
- [OpenLDAP Documentation](https://www.openldap.org/doc/)
- [LDAP on Docker Hub](https://hub.docker.com/r/osixia/openldap)
- [UV Package Manager](https://github.com/astral-sh/uv)
- [LDAP Tools Guide](https://www.openldap.org/doc/admin24/quickstart.html)
---
**Need Help?** Check the [Troubleshooting](#troubleshooting) section or view logs with `make logs`

134
certs/README.md Normal file
View File

@@ -0,0 +1,134 @@
# SSL/TLS Certificates for LDAP
This directory should contain your SSL/TLS certificates for the LDAP server.
## Required Files
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
## Using Your Custom Dev-CA Certificates
If you maintain your own dev-ca (as mentioned), simply copy your certificates here:
```bash
# Copy your dev-ca generated certificates to this directory
cp /path/to/your/dev-ca/certs/ldap-server.crt ./server.crt
cp /path/to/your/dev-ca/private/ldap-server.key ./server.key
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 certificate can also include SANs (Subject Alternative Names) like:
- `DNS:ldap.testing.local`
- `DNS:localhost`
- `IP:127.0.0.1`
- Ensure the private key is readable by the container (permissions should be 600 or 644)
## Generating Self-Signed Certificates (Quick Start)
If you don't have your dev-ca handy and want to quickly test, you can generate self-signed certificates:
### Option 1: Using OpenSSL (Manual)
```bash
# Generate CA private key
openssl genrsa -out ca.key 4096
# Generate CA certificate
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/C=US/ST=State/L=City/O=Testing Org/CN=Testing CA"
# Generate server private key
openssl genrsa -out server.key 4096
# Generate server certificate signing request
openssl req -new -key server.key -out server.csr \
-subj "/C=US/ST=State/L=City/O=Testing Org/CN=ldap.testing.local"
# Create extensions file for SAN
cat > server-ext.cnf <<EOF
subjectAltName = DNS:ldap.testing.local,DNS:localhost,IP:127.0.0.1
extendedKeyUsage = serverAuth,clientAuth
EOF
# Sign the server certificate with the CA
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 365 \
-extfile server-ext.cnf
# Clean up temporary files
rm server.csr ca.key server-ext.cnf ca.srl
```
### Option 2: Using the Provided Script
```bash
# Run the certificate generation script from the project root
python scripts/generate_certs.py
```
## File Permissions
Ensure proper permissions for security:
```bash
chmod 644 ca.crt
chmod 644 server.crt
chmod 600 server.key
```
## Verifying Your Certificates
After placing your certificates, verify them:
```bash
# Check certificate details
openssl x509 -in server.crt -text -noout
# Verify certificate chain
openssl verify -CAfile ca.crt server.crt
# Check certificate and key match
openssl x509 -noout -modulus -in server.crt | openssl md5
openssl rsa -noout -modulus -in server.key | openssl md5
# The MD5 hashes should match
```
## Testing LDAPS Connection
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 with ldapsearch
ldapsearch -H ldaps://localhost:636 -x -b "dc=testing,dc=local" \
-D "cn=admin,dc=testing,dc=local" -w admin_password
```
## Troubleshooting
### Certificate Errors
If you see TLS/SSL errors in the logs:
1. Verify the certificate hostname matches `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
### Container Won't Start
If the container fails to start:
1. Check Docker logs: `docker-compose logs openldap`
2. Verify file permissions on certificate files
3. Ensure certificates are in PEM format (not DER or other formats)
## Security Note
These certificates are for **development use only**. Never use self-signed or development certificates in production environments.

75
docker-compose.yml Normal file
View File

@@ -0,0 +1,75 @@
version: '3.8'
services:
openldap:
image: osixia/openldap:1.5.0
container_name: ldap-server
hostname: ldap.testing.local
environment:
# Base domain configuration
LDAP_ORGANISATION: "Testing Organization"
LDAP_DOMAIN: "testing.local"
LDAP_BASE_DN: "dc=testing,dc=local"
# Admin credentials (change these for production)
LDAP_ADMIN_PASSWORD: "admin_password"
LDAP_CONFIG_PASSWORD: "config_password"
# SSL/TLS Configuration
LDAP_TLS: "true"
LDAP_TLS_CRT_FILENAME: "server.crt"
LDAP_TLS_KEY_FILENAME: "server.key"
LDAP_TLS_CA_CRT_FILENAME: "ca.crt"
LDAP_TLS_VERIFY_CLIENT: "try"
# Logging
LDAP_LOG_LEVEL: "256"
ports:
# Standard LDAP port
- "389:389"
# LDAPS (SSL) port
- "636:636"
volumes:
# Custom certificates - place your dev-ca certs here
- ./certs:/container/service/slapd/assets/certs:ro
# LDIF files for initial data population
- ./ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom:ro
# Persistent data storage
- ldap_data:/var/lib/ldap
- ldap_config:/etc/ldap/slapd.d
networks:
- ldap-network
restart: unless-stopped
command: --copy-service --loglevel debug
# Optional: phpLDAPadmin for web-based management
phpldapadmin:
image: osixia/phpldapadmin:0.9.0
container_name: ldap-admin
environment:
PHPLDAPADMIN_LDAP_HOSTS: "openldap"
PHPLDAPADMIN_HTTPS: "false"
ports:
- "8080:80"
depends_on:
- openldap
networks:
- ldap-network
restart: unless-stopped
volumes:
ldap_data:
driver: local
ldap_config:
driver: local
networks:
ldap-network:
driver: bridge

336
examples/README.md Normal file
View File

@@ -0,0 +1,336 @@
# LDAP Docker Examples
This directory contains example scripts and applications demonstrating how to use the LDAP server for authentication and user management.
## Available Examples
### 1. Simple Authentication (`simple_auth.py`)
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
- List all users in the directory
**Usage:**
```bash
# Authenticate a user (default: jdoe)
python examples/simple_auth.py
# Authenticate with custom credentials
python examples/simple_auth.py --username jsmith --password password123
# List all users
python examples/simple_auth.py --list-users
# Use a different LDAP server
python examples/simple_auth.py --server ldaps://localhost:636
```
**Example Output:**
```
🔐 LDAP Authentication Example
Server: ldap://localhost:389
Attempting to authenticate user: jdoe
✅ Authentication successful for user: jdoe
✅ Authentication successful!
Fetching user information...
==================================================
USER INFORMATION
==================================================
Username: jdoe
Full Name: John Doe
First Name: John
Last Name: Doe
Email: jdoe@testing.local
UID Number: 10001
GID Number: 10001
DN: uid=jdoe,ou=people,dc=testing,dc=local
==================================================
Fetching user groups...
User belongs to 2 group(s):
• developers
• users
```
## Using in Your Application
### Python with ldap3
```python
from ldap3 import Server, Connection
# Connect and authenticate
server = Server('ldap://localhost:389')
conn = Connection(
server,
user='uid=jdoe,ou=people,dc=testing,dc=local',
password='password123',
auto_bind=True
)
# Search for users
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()
```
### Using ldapsearch (Command Line)
```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" \
"(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" \
"(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!

288
examples/simple_auth.py Normal file
View File

@@ -0,0 +1,288 @@
#!/usr/bin/env python3
"""
Simple LDAP Authentication Example
This script demonstrates how to authenticate users against the LDAP server
and retrieve user information.
Usage:
python examples/simple_auth.py
python examples/simple_auth.py --username jdoe --password password123
"""
import argparse
import sys
from typing import Dict, List, Optional
try:
from ldap3 import ALL, Connection, Server
from ldap3.core.exceptions import LDAPException, LDAPBindError
except ImportError:
print("Error: ldap3 library not found.")
print("Install it with: uv pip install ldap3")
sys.exit(1)
# Configuration
LDAP_SERVER = "ldap://localhost:389"
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"
class LDAPAuthenticator:
"""Simple LDAP authentication helper."""
def __init__(self, server_url: str = LDAP_SERVER, base_dn: str = LDAP_BASE_DN):
"""Initialize the authenticator."""
self.server_url = server_url
self.base_dn = base_dn
self.server = Server(server_url, get_info=ALL)
def authenticate(self, username: str, password: str) -> bool:
"""
Authenticate a user with their username and password.
Args:
username: The user's uid (e.g., 'jdoe')
password: The user's password
Returns:
True if authentication successful, False otherwise
"""
# Construct the user's DN
user_dn = f"uid={username},{LDAP_PEOPLE_OU}"
try:
# Try to bind with the user's credentials
conn = Connection(self.server, user=user_dn, password=password)
if conn.bind():
print(f"✅ Authentication successful for user: {username}")
conn.unbind()
return True
else:
print(f"❌ Authentication failed for user: {username}")
return False
except LDAPBindError as e:
print(f"❌ Authentication failed: Invalid credentials")
return False
except LDAPException as e:
print(f"❌ LDAP error: {e}")
return False
except Exception as e:
print(f"❌ Unexpected error: {e}")
return False
def get_user_info(self, username: str, admin_dn: str, admin_password: str) -> Optional[Dict]:
"""
Retrieve detailed information about a user.
Args:
username: The user's uid
admin_dn: Admin DN for searching
admin_password: Admin password
Returns:
Dictionary with user information or None if not found
"""
try:
conn = Connection(self.server, user=admin_dn, password=admin_password, auto_bind=True)
# Search for the user
conn.search(
search_base=LDAP_PEOPLE_OU,
search_filter=f"(uid={username})",
attributes=["uid", "cn", "sn", "givenName", "mail", "uidNumber", "gidNumber"],
)
if conn.entries:
entry = conn.entries[0]
user_info = {
"username": str(entry.uid),
"full_name": str(entry.cn),
"first_name": str(entry.givenName) if entry.givenName else "",
"last_name": str(entry.sn),
"email": str(entry.mail),
"uid_number": int(entry.uidNumber) if entry.uidNumber else None,
"gid_number": int(entry.gidNumber) if entry.gidNumber else None,
"dn": entry.entry_dn,
}
conn.unbind()
return user_info
else:
print(f"User '{username}' not found")
conn.unbind()
return None
except LDAPException as e:
print(f"Error retrieving user info: {e}")
return None
def get_user_groups(self, username: str, admin_dn: str, admin_password: str) -> List[str]:
"""
Get all groups that a user belongs to.
Args:
username: The user's uid
admin_dn: Admin DN for searching
admin_password: Admin password
Returns:
List of group names
"""
try:
conn = Connection(self.server, user=admin_dn, password=admin_password, auto_bind=True)
# Get user's full DN first
user_dn = f"uid={username},{LDAP_PEOPLE_OU}"
# Search for groups that have this user as a member
conn.search(
search_base=LDAP_GROUPS_OU,
search_filter=f"(member={user_dn})",
attributes=["cn"],
)
groups = [str(entry.cn) for entry in conn.entries]
conn.unbind()
return groups
except LDAPException as e:
print(f"Error retrieving user groups: {e}")
return []
def list_all_users(self, admin_dn: str, admin_password: str) -> List[Dict]:
"""
List all users in the directory.
Args:
admin_dn: Admin DN for searching
admin_password: Admin password
Returns:
List of user dictionaries
"""
try:
conn = Connection(self.server, user=admin_dn, password=admin_password, auto_bind=True)
conn.search(
search_base=LDAP_PEOPLE_OU,
search_filter="(objectClass=inetOrgPerson)",
attributes=["uid", "cn", "mail"],
)
users = []
for entry in conn.entries:
users.append(
{
"username": str(entry.uid),
"full_name": str(entry.cn),
"email": str(entry.mail),
}
)
conn.unbind()
return users
except LDAPException as e:
print(f"Error listing users: {e}")
return []
def print_user_info(user_info: Dict):
"""Pretty print user information."""
print("\n" + "=" * 50)
print("USER INFORMATION")
print("=" * 50)
print(f"Username: {user_info['username']}")
print(f"Full Name: {user_info['full_name']}")
print(f"First Name: {user_info['first_name']}")
print(f"Last Name: {user_info['last_name']}")
print(f"Email: {user_info['email']}")
print(f"UID Number: {user_info['uid_number']}")
print(f"GID Number: {user_info['gid_number']}")
print(f"DN: {user_info['dn']}")
print("=" * 50 + "\n")
def main():
"""Main function demonstrating LDAP authentication."""
parser = argparse.ArgumentParser(description="LDAP Authentication Example")
parser.add_argument(
"--username", "-u", default="jdoe", help="Username to authenticate (default: jdoe)"
)
parser.add_argument(
"--password", "-p", default="password123", help="Password (default: password123)"
)
parser.add_argument(
"--server", "-s", default=LDAP_SERVER, help=f"LDAP server URL (default: {LDAP_SERVER})"
)
parser.add_argument("--list-users", action="store_true", help="List all users")
args = parser.parse_args()
# Admin credentials for retrieving user info
admin_dn = "cn=admin,dc=testing,dc=local"
admin_password = "admin_password"
print(f"\n🔐 LDAP Authentication Example")
print(f"Server: {args.server}\n")
# Initialize authenticator
auth = LDAPAuthenticator(server_url=args.server)
# List all users if requested
if args.list_users:
print("📋 Listing all users...")
users = auth.list_all_users(admin_dn, admin_password)
if users:
print(f"\nFound {len(users)} user(s):\n")
for user in users:
print(f"{user['username']:12s} - {user['full_name']:20s} ({user['email']})")
else:
print("No users found")
print()
return
# Authenticate user
print(f"Attempting to authenticate user: {args.username}")
if auth.authenticate(args.username, args.password):
print("✅ Authentication successful!\n")
# Get detailed user information
print("Fetching user information...")
user_info = auth.get_user_info(args.username, admin_dn, admin_password)
if user_info:
print_user_info(user_info)
# Get user's groups
print("Fetching user groups...")
groups = auth.get_user_groups(args.username, admin_dn, admin_password)
if groups:
print(f"User belongs to {len(groups)} group(s):")
for group in groups:
print(f"{group}")
else:
print("User is not a member of any groups")
print()
else:
print("❌ Authentication failed!")
print("\nAvailable test users:")
print(" • jdoe (password: password123)")
print(" • jsmith (password: password123)")
print(" • testuser (password: password123)")
print(" • admin (password: password123)")
sys.exit(1)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\nInterrupted by user")
sys.exit(0)
except Exception as e:
print(f"\nError: {e}")
sys.exit(1)

109
ldif/01-users.ldif Normal file
View File

@@ -0,0 +1,109 @@
# Sample LDIF file for testing.local domain
# This file creates organizational units, users, and groups for testing
# Create organizational units
dn: ou=people,dc=testing,dc=local
objectClass: organizationalUnit
objectClass: top
ou: people
description: Users in the testing organization
dn: ou=groups,dc=testing,dc=local
objectClass: organizationalUnit
objectClass: top
ou: groups
description: Groups in the testing organization
# Create test users
dn: uid=jdoe,ou=people,dc=testing,dc=local
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
objectClass: top
uid: jdoe
cn: John Doe
sn: Doe
givenName: John
mail: jdoe@testing.local
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ= # password: password123
uidNumber: 10001
gidNumber: 10001
homeDirectory: /home/jdoe
loginShell: /bin/bash
description: Test user - John Doe
dn: uid=jsmith,ou=people,dc=testing,dc=local
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
objectClass: top
uid: jsmith
cn: Jane Smith
sn: Smith
givenName: Jane
mail: jsmith@testing.local
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ= # password: password123
uidNumber: 10002
gidNumber: 10002
homeDirectory: /home/jsmith
loginShell: /bin/bash
description: Test user - Jane Smith
dn: uid=admin,ou=people,dc=testing,dc=local
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
objectClass: top
uid: admin
cn: Admin User
sn: User
givenName: Admin
mail: admin@testing.local
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ= # password: password123
uidNumber: 10000
gidNumber: 10000
homeDirectory: /home/admin
loginShell: /bin/bash
description: Test administrator user
dn: uid=testuser,ou=people,dc=testing,dc=local
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
objectClass: top
uid: testuser
cn: Test User
sn: User
givenName: Test
mail: testuser@testing.local
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ= # password: password123
uidNumber: 10003
gidNumber: 10003
homeDirectory: /home/testuser
loginShell: /bin/bash
description: Generic test user
# Create test groups
dn: cn=developers,ou=groups,dc=testing,dc=local
objectClass: groupOfNames
objectClass: top
cn: developers
description: Development team group
member: uid=jdoe,ou=people,dc=testing,dc=local
member: uid=jsmith,ou=people,dc=testing,dc=local
dn: cn=admins,ou=groups,dc=testing,dc=local
objectClass: groupOfNames
objectClass: top
cn: admins
description: Administrator group
member: uid=admin,ou=people,dc=testing,dc=local
dn: cn=users,ou=groups,dc=testing,dc=local
objectClass: groupOfNames
objectClass: top
cn: users
description: General users group
member: uid=jdoe,ou=people,dc=testing,dc=local
member: uid=jsmith,ou=people,dc=testing,dc=local
member: uid=testuser,ou=people,dc=testing,dc=local

87
pyproject.toml Normal file
View File

@@ -0,0 +1,87 @@
[project]
name = "ldap-docker"
version = "0.1.0"
description = "A development tool for running OpenLDAP with SSL in Docker"
readme = "README.md"
requires-python = ">=3.9"
license = { text = "MIT" }
authors = [
{ name = "Spencer" }
]
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.uv]
dev-dependencies = [
"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"
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by black)
"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"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--verbose",
"--cov=scripts",
"--cov-report=term-missing",
"--cov-report=html",
]

381
quickstart.sh Executable file
View File

@@ -0,0 +1,381 @@
#!/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

13
scripts/__init__.py Normal file
View File

@@ -0,0 +1,13 @@
"""
LDAP Docker Management Scripts
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"]

463
scripts/cli.py Normal file
View File

@@ -0,0 +1,463 @@
#!/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 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 = 389
DEFAULT_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()

299
scripts/generate_certs.py Normal file
View File

@@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""
Generate self-signed SSL/TLS certificates for LDAP server testing.
This script creates a CA certificate and a server certificate for development use.
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 pathlib import Path
try:
from cryptography import x509
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
except ImportError:
print("Error: cryptography library not found.")
print("Install it with: uv pip install cryptography")
sys.exit(1)
def generate_private_key(key_size: int = 4096) -> rsa.RSAPrivateKey:
"""Generate an RSA private key."""
return rsa.generate_private_key(
public_exponent=65537,
key_size=key_size,
backend=default_backend(),
)
def generate_ca_certificate(
private_key: rsa.RSAPrivateKey,
common_name: str = "Testing CA",
days_valid: int = 3650,
) -> x509.Certificate:
"""Generate a self-signed CA certificate."""
subject = issuer = x509.Name(
[
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Development"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Testing Organization"),
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
]
)
certificate = (
x509.CertificateBuilder()
.subject_name(subject)
.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))
.add_extension(
x509.BasicConstraints(ca=True, path_length=None),
critical=True,
)
.add_extension(
x509.KeyUsage(
digital_signature=True,
key_cert_sign=True,
crl_sign=True,
key_encipherment=False,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
encipher_only=False,
decipher_only=False,
),
critical=True,
)
.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False,
)
.sign(private_key, hashes.SHA256(), default_backend())
)
return certificate
def generate_server_certificate(
private_key: rsa.RSAPrivateKey,
ca_cert: x509.Certificate,
ca_key: rsa.RSAPrivateKey,
hostname: str = "ldap.testing.local",
san_list: list[str] = None,
days_valid: int = 365,
) -> x509.Certificate:
"""Generate a server certificate signed by the CA."""
if san_list is None:
san_list = [
"ldap.testing.local",
"localhost",
]
subject = x509.Name(
[
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Development"),
x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Testing Organization"),
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
]
)
# Build Subject Alternative Names
san_entries = []
for name in san_list:
san_entries.append(x509.DNSName(name))
# Add IP addresses
san_entries.append(x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")))
certificate = (
x509.CertificateBuilder()
.subject_name(subject)
.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))
.add_extension(
x509.SubjectAlternativeName(san_entries),
critical=False,
)
.add_extension(
x509.BasicConstraints(ca=False, path_length=None),
critical=True,
)
.add_extension(
x509.KeyUsage(
digital_signature=True,
key_encipherment=True,
key_cert_sign=False,
crl_sign=False,
content_commitment=False,
data_encipherment=False,
key_agreement=False,
encipher_only=False,
decipher_only=False,
),
critical=True,
)
.add_extension(
x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]),
critical=False,
)
.add_extension(
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
critical=False,
)
.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
critical=False,
)
.sign(ca_key, hashes.SHA256(), default_backend())
)
return certificate
def save_private_key(key: rsa.RSAPrivateKey, filepath: Path) -> None:
"""Save a private key to a file."""
pem = key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
filepath.write_bytes(pem)
filepath.chmod(0o600) # Set restrictive permissions
print(f"✓ Private key saved: {filepath}")
def save_certificate(cert: x509.Certificate, filepath: Path) -> None:
"""Save a certificate to a file."""
pem = cert.public_bytes(serialization.Encoding.PEM)
filepath.write_bytes(pem)
filepath.chmod(0o644)
print(f"✓ Certificate saved: {filepath}")
def main():
parser = argparse.ArgumentParser(
description="Generate SSL/TLS certificates for LDAP development server"
)
parser.add_argument(
"--output-dir",
type=Path,
default=Path(__file__).parent.parent / "certs",
help="Output directory for certificates (default: ./certs)",
)
parser.add_argument(
"--hostname",
default="ldap.testing.local",
help="Server hostname (default: ldap.testing.local)",
)
parser.add_argument(
"--san",
action="append",
help="Additional Subject Alternative Names (can be used multiple times)",
)
parser.add_argument(
"--force",
action="store_true",
help="Overwrite existing certificates",
)
parser.add_argument(
"--ca-days",
type=int,
default=3650,
help="CA certificate validity in days (default: 3650)",
)
parser.add_argument(
"--server-days",
type=int,
default=365,
help="Server certificate validity in days (default: 365)",
)
args = parser.parse_args()
# Ensure output directory exists
args.output_dir.mkdir(parents=True, exist_ok=True)
# Define file paths
ca_key_path = args.output_dir / "ca.key"
ca_cert_path = args.output_dir / "ca.crt"
server_key_path = args.output_dir / "server.key"
server_cert_path = args.output_dir / "server.crt"
# 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()
]
if existing:
print("Error: The following certificate files already exist:")
for p in existing:
print(f" - {p}")
print("\nUse --force to overwrite existing certificates")
sys.exit(1)
print("Generating certificates for LDAP server...")
print(f"Hostname: {args.hostname}")
print(f"Output directory: {args.output_dir}")
print()
# Generate CA
print("Step 1: Generating CA certificate...")
ca_key = generate_private_key()
ca_cert = generate_ca_certificate(ca_key, days_valid=args.ca_days)
save_private_key(ca_key, ca_key_path)
save_certificate(ca_cert, ca_cert_path)
print()
# Generate Server Certificate
print("Step 2: Generating server certificate...")
server_key = generate_private_key()
san_list = [args.hostname, "localhost"]
if args.san:
san_list.extend(args.san)
server_cert = generate_server_certificate(
server_key,
ca_cert,
ca_key,
hostname=args.hostname,
san_list=san_list,
days_valid=args.server_days,
)
save_private_key(server_key, server_key_path)
save_certificate(server_cert, server_cert_path)
print()
print("✅ Certificate generation complete!")
print()
print("Generated files:")
print(f" - CA Certificate: {ca_cert_path}")
print(f" - CA Private Key: {ca_key_path} (keep secure!)")
print(f" - Server Certificate: {server_cert_path}")
print(f" - Server Private Key: {server_key_path} (keep secure!)")
print()
print("Next steps:")
print(" 1. Keep the CA private key secure (you can delete it if not needed)")
print(" 2. Start the LDAP server: docker-compose up -d")
print(" 3. Test the connection: ldapsearch -H ldaps://localhost:636 -x")
print()
print("Note: These certificates are for DEVELOPMENT ONLY!")
if __name__ == "__main__":
main()

8
tests/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
"""
Tests for LDAP Docker Management Tools
This package contains unit and integration tests for the LDAP Docker
development environment management scripts.
"""
__version__ = "0.1.0"

View File

@@ -0,0 +1,252 @@
"""
Tests for certificate generation script.
This module contains unit tests for the generate_certs.py script.
"""
import tempfile
from datetime import datetime, timedelta
from pathlib import Path
import pytest
try:
from cryptography import x509
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
CRYPTO_AVAILABLE = True
except ImportError:
CRYPTO_AVAILABLE = False
# Import the module to test
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
if CRYPTO_AVAILABLE:
from scripts.generate_certs import (
generate_ca_certificate,
generate_private_key,
generate_server_certificate,
save_certificate,
save_private_key,
)
@pytest.mark.skipif(not CRYPTO_AVAILABLE, reason="cryptography library not available")
class TestCertificateGeneration:
"""Tests for certificate generation functions."""
def test_generate_private_key(self):
"""Test that a private key can be generated."""
key = generate_private_key(key_size=2048)
assert isinstance(key, rsa.RSAPrivateKey)
assert key.key_size == 2048
def test_generate_private_key_default_size(self):
"""Test that default key size is 4096."""
key = generate_private_key()
assert isinstance(key, rsa.RSAPrivateKey)
assert key.key_size == 4096
def test_generate_ca_certificate(self):
"""Test that a CA certificate can be generated."""
key = generate_private_key(key_size=2048)
cert = generate_ca_certificate(key, common_name="Test CA", days_valid=365)
assert isinstance(cert, x509.Certificate)
assert cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value == "Test CA"
# Check that it's self-signed
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
def test_generate_ca_certificate_validity(self):
"""Test that CA certificate has correct validity period."""
key = generate_private_key(key_size=2048)
days_valid = 365
cert = generate_ca_certificate(key, days_valid=days_valid)
now = datetime.utcnow()
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
def test_generate_server_certificate(self):
"""Test that a server certificate can be generated."""
ca_key = generate_private_key(key_size=2048)
ca_cert = generate_ca_certificate(ca_key, common_name="Test CA")
server_key = generate_private_key(key_size=2048)
server_cert = generate_server_certificate(
server_key,
ca_cert,
ca_key,
hostname="test.example.com",
days_valid=365,
)
assert isinstance(server_cert, x509.Certificate)
assert (
server_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
== "test.example.com"
)
# Check that it's signed by the CA
assert server_cert.issuer == ca_cert.subject
# Check basic constraints - should not be a CA
basic_constraints = server_cert.extensions.get_extension_for_oid(
x509.ExtensionOID.BASIC_CONSTRAINTS
)
assert basic_constraints.value.ca is False
def test_generate_server_certificate_san(self):
"""Test that server certificate includes Subject Alternative Names."""
ca_key = generate_private_key(key_size=2048)
ca_cert = generate_ca_certificate(ca_key)
server_key = generate_private_key(key_size=2048)
san_list = ["test.example.com", "localhost", "test.local"]
server_cert = generate_server_certificate(
server_key,
ca_cert,
ca_key,
hostname="test.example.com",
san_list=san_list,
)
# Get SAN extension
san_ext = server_cert.extensions.get_extension_for_oid(
x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
)
# Extract DNS names
dns_names = [name.value for name in san_ext.value if isinstance(name, x509.DNSName)]
# Check that all DNS names are present
for name in san_list:
assert name in dns_names
def test_save_private_key(self):
"""Test that a private key can be saved to a file."""
key = generate_private_key(key_size=2048)
with tempfile.TemporaryDirectory() as tmpdir:
key_path = Path(tmpdir) / "test.key"
save_private_key(key, key_path)
assert key_path.exists()
assert key_path.stat().st_size > 0
# Check file permissions (on Unix-like systems)
if hasattr(key_path.stat(), "st_mode"):
mode = key_path.stat().st_mode & 0o777
assert mode == 0o600, f"Expected 0o600 but got {oct(mode)}"
def test_save_certificate(self):
"""Test that a certificate can be saved to a file."""
key = generate_private_key(key_size=2048)
cert = generate_ca_certificate(key)
with tempfile.TemporaryDirectory() as tmpdir:
cert_path = Path(tmpdir) / "test.crt"
save_certificate(cert, cert_path)
assert cert_path.exists()
assert cert_path.stat().st_size > 0
# Check file permissions
if hasattr(cert_path.stat(), "st_mode"):
mode = cert_path.stat().st_mode & 0o777
assert mode == 0o644, f"Expected 0o644 but got {oct(mode)}"
def test_full_certificate_chain(self):
"""Test generating a complete certificate chain."""
# Generate CA
ca_key = generate_private_key(key_size=2048)
ca_cert = generate_ca_certificate(ca_key, common_name="Test Root CA")
# Generate server certificate
server_key = generate_private_key(key_size=2048)
server_cert = generate_server_certificate(
server_key,
ca_cert,
ca_key,
hostname="ldap.testing.local",
san_list=["ldap.testing.local", "localhost"],
)
# Verify the chain
assert server_cert.issuer == ca_cert.subject
assert ca_cert.subject == ca_cert.issuer # Self-signed
# Verify server cert is not a CA
server_basic_constraints = server_cert.extensions.get_extension_for_oid(
x509.ExtensionOID.BASIC_CONSTRAINTS
)
assert 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
)
assert ca_basic_constraints.value.ca is True
@pytest.mark.skipif(not CRYPTO_AVAILABLE, reason="cryptography library not available")
class TestCertificateValidation:
"""Tests for certificate validation and properties."""
def test_certificate_has_correct_extensions(self):
"""Test that generated certificates have correct extensions."""
ca_key = generate_private_key(key_size=2048)
ca_cert = generate_ca_certificate(ca_key)
server_key = generate_private_key(key_size=2048)
server_cert = generate_server_certificate(
server_key, ca_cert, ca_key, hostname="test.local"
)
# 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
def test_certificate_validity_dates(self):
"""Test that certificates have correct validity dates."""
key = generate_private_key(key_size=2048)
days_valid = 100
cert = generate_ca_certificate(key, days_valid=days_valid)
now = datetime.utcnow()
# Check not_valid_before is around now
assert abs((cert.not_valid_before - 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
def test_imports():
"""Test that all required imports are available."""
if CRYPTO_AVAILABLE:
assert x509 is not None
assert hashes is not None
assert rsa is not None
else:
pytest.skip("cryptography library not available")