First commit from the robot
This commit is contained in:
102
.env.example
Normal file
102
.env.example
Normal 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
117
.gitignore
vendored
Normal 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
288
GETTING_STARTED.md
Normal 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
187
Makefile
Normal 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
354
QUICKREF.md
Normal 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
459
README.md
Normal 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
134
certs/README.md
Normal 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
75
docker-compose.yml
Normal 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
336
examples/README.md
Normal 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
288
examples/simple_auth.py
Normal 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
109
ldif/01-users.ldif
Normal 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
87
pyproject.toml
Normal 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
381
quickstart.sh
Executable 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
13
scripts/__init__.py
Normal 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
463
scripts/cli.py
Normal 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
299
scripts/generate_certs.py
Normal 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
8
tests/__init__.py
Normal 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"
|
||||
252
tests/test_generate_certs.py
Normal file
252
tests/test_generate_certs.py
Normal 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")
|
||||
Reference in New Issue
Block a user