diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8c03d45 --- /dev/null +++ b/.env.example @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1eed54e --- /dev/null +++ b/.gitignore @@ -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 diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md new file mode 100644 index 0000000..2b1e1da --- /dev/null +++ b/GETTING_STARTED.md @@ -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 +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`. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5450c72 --- /dev/null +++ b/Makefile @@ -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 || 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" diff --git a/QUICKREF.md b/QUICKREF.md new file mode 100644 index 0000000..6d6ad63 --- /dev/null +++ b/QUICKREF.md @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e4a0513 --- /dev/null +++ b/README.md @@ -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` diff --git a/certs/README.md b/certs/README.md new file mode 100644 index 0000000..a9f4436 --- /dev/null +++ b/certs/README.md @@ -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 < 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! \ No newline at end of file diff --git a/examples/simple_auth.py b/examples/simple_auth.py new file mode 100644 index 0000000..75adb9b --- /dev/null +++ b/examples/simple_auth.py @@ -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) diff --git a/ldif/01-users.ldif b/ldif/01-users.ldif new file mode 100644 index 0000000..fe52c0c --- /dev/null +++ b/ldif/01-users.ldif @@ -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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2521982 --- /dev/null +++ b/pyproject.toml @@ -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", +] diff --git a/quickstart.sh b/quickstart.sh new file mode 100755 index 0000000..76909de --- /dev/null +++ b/quickstart.sh @@ -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 diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e0b4177 --- /dev/null +++ b/scripts/__init__.py @@ -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"] diff --git a/scripts/cli.py b/scripts/cli.py new file mode 100644 index 0000000..61aab55 --- /dev/null +++ b/scripts/cli.py @@ -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() diff --git a/scripts/generate_certs.py b/scripts/generate_certs.py new file mode 100644 index 0000000..ca94e96 --- /dev/null +++ b/scripts/generate_certs.py @@ -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() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..9072c47 --- /dev/null +++ b/tests/__init__.py @@ -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" diff --git a/tests/test_generate_certs.py b/tests/test_generate_certs.py new file mode 100644 index 0000000..9651ae6 --- /dev/null +++ b/tests/test_generate_certs.py @@ -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")