diff --git a/CHANGELOG.md b/CHANGELOG.md index 28c97b7..ec39274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,90 +5,135 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.1.0] - 2025-01-XX +## [0.1.0] - 2025-10-20 ### Added + - Initial release of LDAP Docker development tool - OpenLDAP 1.5.0 container with SSL/TLS support - phpLDAPadmin web interface for easy administration - Pre-configured test users and groups for testing.local domain - SSL certificate generation script using Python cryptography -- Comprehensive CLI tool for managing LDAP server (`ldap-docker` command) - Makefile with convenient shortcuts for common operations -- Interactive quickstart script (`quickstart.sh`) for guided setup -- Example Python authentication script demonstrating LDAP integration +- Example Python authentication script demonstrating LDAP integration with environment variable support - Support for custom dev-ca certificates - Persistent Docker volumes for data and configuration - Test suite for certificate generation +- Comprehensive environment variable reference table in Configuration section with usage cross-references +- `{.env:VARIABLE_NAME}` syntax throughout documentation to indicate configurable values +- Explanatory notes about `.env` configuration flexibility at key sections - Comprehensive documentation: - - README.md - Full project documentation - - GETTING_STARTED.md - Beginner-friendly guide - - QUICKREF.md - Quick command reference + - README.md - Full project documentation with integrated configuration guide - certs/README.md - Certificate management guide - - examples/README.md - Integration patterns and examples + - examples/README.md - Integration patterns and code examples + +### Changed + +- Renamed Makefile targets from `test-*` to `verify-*` for integration checks that require a running container + - `test-connection` → `verify-connection` + - `test-auth` → `verify-auth` + - `test-users` → `verify-users` + - `test-ssl` → `verify-ssl` + - `test-all` → `verify-all` +- Added separate `make test` and `make test-cov` targets for running unit tests with pytest +- Improved naming clarity: "verify" commands check running containers, "test" commands run unit tests ### Test Data + - 4 pre-configured test users (admin, jdoe, jsmith, testuser) - 3 test groups (admins, developers, users) - All test users use password: `password123` - Admin credentials: `cn=admin,dc=testing,dc=local` / `admin_password` ### Infrastructure + - Docker Compose configuration for easy deployment - UV package manager integration for Python dependencies +- `.env` file support for configuration - Cross-platform support (MacOS, Linux, Windows) - Rancher Desktop and Docker Desktop compatibility +### Configuration + +- Environment variable support for all configurable values +- `.env.example` file with comprehensive documentation +- Configurable ports (LDAP, LDAPS, phpLDAPadmin) +- Configurable domain and organization settings +- Configurable SSL/TLS certificate filenames +- Configurable admin credentials + +### Documentation + +- README with clear Quick Start, Configuration, and Troubleshooting sections +- All code examples (Python, bash, ldapsearch) reference `.env` variables +- examples/README.md with `.env` variable references in all code samples +- certs/README.md with `.env` variables for hostnames, ports, and certificate filenames +- Admin Credentials, Testing Authentication, and Certificate Requirements sections use `.env` syntax +- Default Test Users table shows dynamic email addresses based on `LDAP_DOMAIN` +- Development section LDIF examples reference `.env` variables + ### Fixed + - Updated `pyproject.toml` to use `dependency-groups.dev` instead of deprecated `tool.uv.dev-dependencies` - Added `tool.hatch.build.targets.wheel.packages` configuration to fix build errors - Removed obsolete `version` field from `docker-compose.yml` (Docker Compose v2+ compatibility) - Fixed LDAP user password hashes to use proper SSHA format generated by `slappasswd` - Fixed attribute type conversion in example scripts for uidNumber and gidNumber +- Fixed `scripts/__init__.py` import error that prevented pytest from running +- Fixed timezone-aware datetime comparisons in certificate generation tests (updated to use `not_valid_before_utc` and `not_valid_after_utc`) ### Technical Details -- Base DN: `dc=testing,dc=local` -- LDAP Port: 389 (standard) -- LDAPS Port: 636 (SSL/TLS) -- Web Admin Port: 8080 -- Python 3.9+ required + +- Base DN: `dc=testing,dc=local` (configurable via `LDAP_BASE_DN`) +- LDAP Port: 389 (configurable via `LDAP_PORT`) +- LDAPS Port: 636 (configurable via `LDAPS_PORT`) +- Web Admin Port: 8080 (configurable via `PHPLDAPADMIN_PORT`) +- Python 3.9+ required (optional, for certificate generation and examples) - Docker/Rancher Desktop required ## [Unreleased] ### Planned Features -- Additional integration examples (Node.js, Go, Ruby, etc.) -- Health check endpoints -- Automated backup scripts + - Docker image with pre-built configuration -- Kubernetes/Helm deployment examples -- LDAP replication setup guide -- Performance tuning guide -- Security hardening options --- ## Release Notes ### Version 0.1.0 + This is the initial release providing a complete LDAP development environment suitable for: + - Testing LDAP authentication in applications - Development and integration testing - Learning LDAP concepts - Prototyping LDAP-based systems +**Key Features:** + +- Quick setup with `docker-compose up -d` +- Fully configurable via `.env` file +- Pre-populated with test users and groups +- SSL/TLS support with custom certificate capability +- Web-based administration interface +- Comprehensive documentation with clear examples + **Important Security Notes:** + - This tool is for **DEVELOPMENT USE ONLY** - Default passwords are well-known and insecure - Self-signed certificates are not suitable for production - Never use this with real user data or in production environments ### Upgrade Instructions + Not applicable for initial release. ### Breaking Changes + Not applicable for initial release. --- -For support, issues, or feature requests, please refer to the project documentation or open an issue on the project repository. \ No newline at end of file +For support, issues, or feature requests, please refer to the project documentation or open an issue on the project repository. diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md deleted file mode 100644 index 2b1e1da..0000000 --- a/GETTING_STARTED.md +++ /dev/null @@ -1,288 +0,0 @@ -# Getting Started with LDAP Docker - -Welcome! This guide will help you get your LDAP development server up and running in just a few minutes. - -## What is This? - -LDAP Docker is a ready-to-use OpenLDAP server for development and testing. It comes pre-configured with: - -- ✅ SSL/TLS support (LDAPS) -- ✅ Test users and groups -- ✅ Web-based admin interface -- ✅ Easy certificate management -- ✅ Simple Python CLI tools - -Perfect for testing applications that need LDAP authentication without setting up a complex infrastructure. - -## Prerequisites - -You'll need these installed on your Mac: - -1. **Docker or Rancher Desktop** (for running containers) - - Rancher Desktop: https://rancherdesktop.io/ (recommended for Mac) - - Or Docker Desktop: https://www.docker.com/products/docker-desktop - -2. **Python 3.9+** (usually pre-installed on Mac) - ```bash - python3 --version - ``` - -3. **UV Package Manager** (optional but recommended) - ```bash - curl -LsSf https://astral.sh/uv/install.sh | sh - ``` - -## Quick Start (5 Minutes) - -### Option 1: Automated Setup (Easiest) - -Run the quick start script that will guide you through everything: - -```bash -./quickstart.sh -``` - -This interactive script will: -1. Check all prerequisites -2. Install UV (if you want) -3. Generate SSL certificates -4. Start the LDAP server -5. Verify everything works - -### Option 2: Manual Setup (Step by Step) - -If you prefer to understand each step: - -```bash -# 1. Install Python dependencies -make install - -# 2. Generate SSL certificates -make certs-generate - -# 3. Start the LDAP server -make start - -# 4. Test the connection -make test-connection -``` - -That's it! Your LDAP server is now running. - -## Using Your Own Dev-CA Certificates - -Since you mentioned you maintain a local dev-ca, here's how to use your own certificates: - -```bash -# Instead of 'make certs-generate', copy your certificates: -cp /path/to/your/dev-ca/ca-cert.pem certs/ca.crt -cp /path/to/your/dev-ca/ldap-server.crt certs/server.crt -cp /path/to/your/dev-ca/ldap-server.key certs/server.key - -# Set proper permissions -chmod 644 certs/ca.crt certs/server.crt -chmod 600 certs/server.key - -# Then start the server -make start -``` - -**Important:** Your server certificate should be issued for hostname `ldap.testing.local` or include it as a Subject Alternative Name (SAN). - -## Accessing Your LDAP Server - -Once running, you can access: - -| Service | URL | Purpose | -|---------|-----|---------| -| **LDAP** | `ldap://localhost:389` | Standard LDAP (unencrypted) | -| **LDAPS** | `ldaps://localhost:636` | Secure LDAP with SSL/TLS | -| **Admin UI** | `http://localhost:8080` | Web interface (phpLDAPadmin) | - -### Login Credentials - -- **Admin DN:** `cn=admin,dc=testing,dc=local` -- **Password:** `admin_password` - -### Test Users (All use password: `password123`) - -- `jdoe` - John Doe (jdoe@testing.local) -- `jsmith` - Jane Smith (jsmith@testing.local) -- `testuser` - Test User (testuser@testing.local) -- `admin` - Admin User (admin@testing.local) - -## Verify Everything Works - -### Test 1: List all users -```bash -make test-users -``` - -You should see output like: -``` -Found 4 user(s): - - - Admin User: admin (admin@testing.local) - - John Doe: jdoe (jdoe@testing.local) - - Jane Smith: jsmith (jsmith@testing.local) - - Test User: testuser (testuser@testing.local) -``` - -### Test 2: Try the web interface - -Open http://localhost:8080 in your browser and login with: -- Login DN: `cn=admin,dc=testing,dc=local` -- Password: `admin_password` - -### Test 3: Run the example script - -```bash -python examples/simple_auth.py -``` - -This authenticates user `jdoe` and displays their information. - -## Common Commands - -```bash -# Server management -make start # Start LDAP server -make stop # Stop LDAP server -make restart # Restart LDAP server -make logs # View logs (live) -make status # Check if running - -# Testing -make test-users # List all users -make test-auth # Test authentication -make test-all # Run all tests - -# Maintenance -make clean # Clean build artifacts -make down # Stop and remove containers -``` - -## Next Steps - -Now that your LDAP server is running, you can: - -1. **Integrate with Your Application** - - See `examples/README.md` for code samples - - Point your app to `ldap://localhost:389` - -2. **Add Custom Users** - - Edit `ldif/01-users.ldif` - - Run `make down-volumes && make start` to reload - -3. **Configure for Your Use Case** - - Edit `docker-compose.yml` for custom settings - - See `.env.example` for environment variables - -4. **Learn More** - - `README.md` - Full documentation - - `QUICKREF.md` - Command reference - - `certs/README.md` - Certificate management - - `examples/README.md` - Integration examples - -## Troubleshooting - -### "Docker is not running" -Start Rancher Desktop from your Applications folder. Look for its icon in the menu bar. - -### "Connection refused" -Wait 10-30 seconds after starting - the server needs time to initialize: -```bash -make logs # Watch until you see "slapd starting" -``` - -### "Certificate errors" -Verify certificates exist: -```bash -ls -la certs/ -``` - -Regenerate if needed: -```bash -make certs-generate --force -``` - -### "Port already in use" -Check if something is using LDAP ports: -```bash -lsof -i :389 -lsof -i :636 -``` - -### Still stuck? -Check the full troubleshooting section in `README.md` or view logs: -```bash -make logs -``` - -## Docker Basics (If You're New) - -Since you mentioned being new to Docker, here are the basics: - -- **Container**: A lightweight, isolated environment running your LDAP server -- **Image**: The blueprint for creating containers (we use `osixia/openldap`) -- **Volume**: Persistent storage for LDAP data (survives restarts) -- **docker-compose**: Tool for managing multi-container applications (LDAP + Admin UI) - -When you run `make start`, Docker: -1. Downloads the LDAP image (first time only) -2. Creates containers from the image -3. Mounts your certificates and data files -4. Starts the LDAP service - -The `Makefile` is just a collection of shortcuts for common Docker commands. - -## Building Elsewhere - -This project works on: -- ✅ **MacOS** (with Rancher Desktop or Docker Desktop) -- ✅ **Linux** (with Docker installed) -- ✅ **Windows** (with Docker Desktop + WSL2) - -The same `docker-compose.yml` works everywhere - that's the beauty of Docker! - -To share your setup with a colleague: -```bash -# Just copy the whole project -git clone -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 index 1820afd..c272e67 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install init start stop restart down logs status certs-generate certs-check test-connection test-auth test-users clean clean-all +.PHONY: help install init start stop restart down logs status certs-generate certs-check verify-connection verify-auth verify-users verify-ssl verify-all test test-cov clean clean-all # Load environment variables from .env file if it exists ifneq (,$(wildcard ./.env)) @@ -15,7 +15,7 @@ help: ## Show this help message @echo "Usage: make [target]" @echo "" @echo "Available targets:" - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' install: ## Install Python dependencies with UV @echo "Installing dependencies with UV..." @@ -25,7 +25,7 @@ install: ## Install Python dependencies with UV install-dev: ## Install development dependencies @echo "Installing development dependencies with UV..." - uv sync --all-extras + uv sync --group dev @echo "✅ Development dependencies installed" init: install certs-check ## Initialize the environment (install deps, check certs) @@ -109,23 +109,33 @@ logs-admin: ## View phpLDAPadmin logs status: ## Show container status @docker-compose ps -test-connection: ## Test connection to LDAP server - @echo "Testing LDAP connection..." +verify-connection: ## Verify connection to LDAP server (requires running container) + @echo "Verifying LDAP connection..." @export LDAP_PORT=$${LDAP_PORT:-389}; uv run python -c "import os; from ldap3 import Server, Connection, ALL; s = Server(f\"ldap://localhost:{os.environ.get('LDAP_PORT', '389')}\", get_info=ALL); c = Connection(s, auto_bind=True); print('✅ Connection successful'); c.unbind()" -test-auth: ## Test authentication with admin user - @echo "Testing LDAP authentication..." +verify-auth: ## Verify authentication with admin user (requires running container) + @echo "Verifying LDAP authentication..." @export LDAP_PORT=$${LDAP_PORT:-389}; uv run python -c "import os; from ldap3 import Server, Connection; s = Server(f\"ldap://localhost:{os.environ.get('LDAP_PORT', '389')}\"); c = Connection(s, 'cn=admin,dc=testing,dc=local', 'admin_password', auto_bind=True); print('✅ Authentication successful'); c.unbind()" -test-users: ## List all users in LDAP +verify-users: ## List all users in LDAP (requires running container) @echo "Listing LDAP users..." @export LDAP_PORT=$${LDAP_PORT:-389}; uv run python -c "import os; from ldap3 import Server, Connection; s = Server(f\"ldap://localhost:{os.environ.get('LDAP_PORT', '389')}\"); c = Connection(s, 'cn=admin,dc=testing,dc=local', 'admin_password', auto_bind=True); c.search('dc=testing,dc=local', '(objectClass=inetOrgPerson)', attributes=['uid', 'cn', 'mail']); [print(f' - {e.cn}: {e.uid} ({e.mail})') for e in c.entries]; c.unbind()" -test-ssl: ## Test SSL/TLS connection - @echo "Testing LDAPS connection..." +verify-ssl: ## Verify SSL/TLS connection (requires running container) + @echo "Verifying LDAPS connection..." openssl s_client -connect localhost:$${LDAPS_PORT:-636} -CAfile certs/ca.crt /dev/null || true find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true + find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true find . -type f -name ".coverage" -delete 2>/dev/null || true rm -rf build/ dist/ .eggs/ @@ -158,8 +168,8 @@ dev-setup: install-dev certs-generate start ## Complete development setup @echo "" @echo "Next steps:" @echo " - View logs: make logs" - @echo " - Test connection: make test-connection" - @echo " - List users: make test-users" + @echo " - Verify connection: make verify-connection" + @echo " - List users: make verify-users" @echo " - Open admin UI: open http://localhost:8080" quick-start: certs-check start ## Quick start (assumes certs exist) diff --git a/PORT_CONFIGURATION.md b/PORT_CONFIGURATION.md deleted file mode 100644 index 968ab99..0000000 --- a/PORT_CONFIGURATION.md +++ /dev/null @@ -1,436 +0,0 @@ -# Port Configuration Guide - -This guide explains how to configure custom ports for your LDAP Docker environment to avoid conflicts with other services. - -## Quick Start - -**To change ports, simply edit the `.env` file:** - -```bash -# Edit .env file -LDAP_PORT=20389 -LDAPS_PORT=20636 -PHPLDAPADMIN_PORT=8080 -``` - -Then restart: -```bash -make down && make start -``` - -That's it! All scripts and tools will automatically use your custom ports. - ---- - -## Why Custom Ports? - -You might want to use custom ports if: -- You have another LDAP server running on standard ports (389/636) -- Port 389 requires root/admin privileges -- You're running multiple LDAP environments simultaneously -- Your organization has specific port requirements - -## Files Involved - -The port configuration system uses these files: - -### 1. `.env` - Your Configuration (Edit This!) -```bash -LDAP_PORT=20389 # Your custom LDAP port -LDAPS_PORT=20636 # Your custom LDAPS port -PHPLDAPADMIN_PORT=8080 # Web admin interface port -``` - -### 2. `docker-compose.yml` - Uses Environment Variables -```yaml -ports: - - "${LDAP_PORT:-389}:389" # Host:Container mapping - - "${LDAPS_PORT:-636}:636" -``` - -The `${LDAP_PORT:-389}` syntax means: -- Use `$LDAP_PORT` if set in `.env` -- Otherwise use default `389` - -### 3. Scripts - Auto-detect Ports -All Python scripts and Makefile commands read from `.env` automatically: -- `scripts/cli.py` -- `scripts/ldapsearch_helper.sh` -- `examples/simple_auth.py` -- `Makefile` - -## How to Change Ports - -### Method 1: Edit .env File (Recommended) - -1. **Edit `.env`:** - ```bash - vim .env # or your favorite editor - ``` - -2. **Change the port values:** - ```bash - LDAP_PORT=20389 - LDAPS_PORT=20636 - ``` - -3. **Restart the containers:** - ```bash - make down && make start - ``` - -4. **Verify:** - ```bash - docker-compose ps - # Should show 0.0.0.0:20389->389/tcp - ``` - -### Method 2: Environment Variables (Temporary) - -For one-time use without modifying `.env`: - -```bash -LDAP_PORT=30389 LDAPS_PORT=30636 docker-compose up -d -``` - -### Method 3: Create .env.local (Advanced) - -For personal overrides without modifying the main `.env`: - -```bash -# Create .env.local (git-ignored) -echo "LDAP_PORT=12389" > .env.local -echo "LDAPS_PORT=12636" >> .env.local - -# Docker Compose will merge both files -docker-compose up -d -``` - -## Testing with Custom Ports - -### Automatic - Use Our Tools - -All our tools automatically detect your ports from `.env`: - -```bash -# These all work automatically with your custom ports -make test-users -make test-connection -uv run python examples/simple_auth.py -``` - -### Generate ldapsearch Commands - -Use our helper script to generate commands with your ports: - -```bash -./scripts/ldapsearch_helper.sh -``` - -Output includes commands like: -```bash -# List all users -ldapsearch -H ldap://localhost:20389 \ - -D "cn=admin,dc=testing,dc=local" \ - -w admin_password \ - -b "ou=people,dc=testing,dc=local" \ - "(objectClass=inetOrgPerson)" \ - uid cn mail -``` - -### Manual ldapsearch Commands - -Replace `389` with your `LDAP_PORT` and `636` with your `LDAPS_PORT`: - -```bash -# Standard LDAP (unencrypted) -ldapsearch -H ldap://localhost:20389 \ - -D "cn=admin,dc=testing,dc=local" \ - -w admin_password \ - -b "dc=testing,dc=local" - -# LDAPS (SSL/TLS) -ldapsearch -H ldaps://localhost:20636 \ - -D "cn=admin,dc=testing,dc=local" \ - -w admin_password \ - -b "dc=testing,dc=local" - -# Search for specific user -ldapsearch -H ldap://localhost:20389 \ - -D "cn=admin,dc=testing,dc=local" \ - -w admin_password \ - -b "dc=testing,dc=local" \ - "(uid=jdoe)" - -# Test user authentication -ldapsearch -H ldap://localhost:20389 \ - -D "uid=jdoe,ou=people,dc=testing,dc=local" \ - -w password123 \ - -b "dc=testing,dc=local" \ - "(uid=jdoe)" -``` - -### Using Python - -```python -import os -from ldap3 import Server, Connection - -# Automatically uses LDAP_PORT from environment -port = os.environ.get('LDAP_PORT', '389') -server = Server(f'ldap://localhost:{port}') -conn = Connection(server, - user='cn=admin,dc=testing,dc=local', - password='admin_password', - auto_bind=True) - -conn.search('dc=testing,dc=local', '(objectClass=*)') -print(f"Connected on port {port}") -conn.unbind() -``` - -## Common Port Configurations - -### Development (Unprivileged Ports) -```bash -LDAP_PORT=20389 -LDAPS_PORT=20636 -PHPLDAPADMIN_PORT=8080 -``` - -### Testing (High Ports) -```bash -LDAP_PORT=30389 -LDAPS_PORT=30636 -PHPLDAPADMIN_PORT=8090 -``` - -### Multiple Environments -```bash -# Environment 1 (.env) -LDAP_PORT=20389 -LDAPS_PORT=20636 - -# Environment 2 (separate directory or .env.local) -LDAP_PORT=21389 -LDAPS_PORT=21636 -``` - -## Troubleshooting - -### Port Already in Use - -**Error:** `Bind for 0.0.0.0:20389 failed: port is already allocated` - -**Check what's using the port:** -```bash -lsof -i :20389 -# or -netstat -an | grep 20389 -``` - -**Solutions:** -1. Stop the conflicting service -2. Choose a different port in `.env` -3. Use `make down` to stop this LDAP server first - -### Scripts Not Using Custom Ports - -**Problem:** Scripts still connect to port 389 - -**Solution:** Ensure `.env` is loaded: - -1. **Check .env exists:** - ```bash - ls -la .env - ``` - -2. **Verify ports are set:** - ```bash - grep LDAP_PORT .env - ``` - -3. **Reload environment:** - ```bash - source .env # For bash scripts - make down && make start # Restart containers - ``` - -### Docker Compose Not Reading .env - -**Problem:** Containers still on default ports - -**Check:** -```bash -# View what Docker Compose sees -docker-compose config | grep -A2 ports -``` - -**Solution:** -```bash -# Ensure .env is in project root -pwd # Should be ldap_docker/ -ls .env # Should exist - -# Force reload -docker-compose down -docker-compose up -d -``` - -### Can't Connect After Port Change - -**Verify containers are running on correct ports:** -```bash -docker-compose ps -# Should show: 0.0.0.0:20389->389/tcp -``` - -**Test connectivity:** -```bash -# Test if port is listening -nc -zv localhost 20389 - -# Test LDAP response -ldapsearch -H ldap://localhost:20389 -x -b "" -s base -``` - -**Check logs:** -```bash -make logs -# Look for: "slapd starting" -``` - -## Port Reference Table - -| Service | Default Port | Docker Internal Port | Configurable Via | -|---------|--------------|---------------------|------------------| -| LDAP | 389 | 389 | `LDAP_PORT` in .env | -| LDAPS | 636 | 636 | `LDAPS_PORT` in .env | -| phpLDAPadmin | 8080 | 80 | `PHPLDAPADMIN_PORT` in .env | - -**Note:** The "Docker Internal Port" (right side of mapping) never changes. Only the host port (left side) is configurable. - -## Advanced: Port Mapping Explained - -Docker port mapping format: `HOST:CONTAINER` - -```yaml -ports: - - "20389:389" -``` - -This means: -- **20389** = Port on your Mac (accessible via `localhost:20389`) -- **389** = Port inside the Docker container (LDAP's standard port) - -The container always listens on 389 internally. We just map it to 20389 externally. - -## Testing Multiple Configurations - -Run multiple LDAP servers simultaneously: - -```bash -# Terminal 1: First instance -cd ldap_docker_1 -echo "LDAP_PORT=20389" > .env -echo "LDAPS_PORT=20636" >> .env -make start - -# Terminal 2: Second instance -cd ldap_docker_2 -echo "LDAP_PORT=21389" > .env -echo "LDAPS_PORT=21636" >> .env -make start - -# Now you have two LDAP servers running! -ldapsearch -H ldap://localhost:20389 ... # Server 1 -ldapsearch -H ldap://localhost:21389 ... # Server 2 -``` - -## Best Practices - -1. **Use .env for configuration** - Don't hardcode ports in scripts -2. **Document your ports** - Add comments in .env explaining your choices -3. **Use unprivileged ports** - Ports above 1024 don't require root -4. **Avoid common ports** - Skip 8080, 3000, 5000 (often used by other tools) -5. **Be consistent** - If LDAP is 20389, make LDAPS 20636 (same prefix) - -## Integration Examples - -### Configure Your Application - -After changing ports, update your application's LDAP configuration: - -```python -# Django settings.py -AUTH_LDAP_SERVER_URI = "ldap://localhost:20389" - -# Flask -LDAP_HOST = "localhost" -LDAP_PORT = 20389 - -# Environment variable -export LDAP_URL="ldap://localhost:20389" -``` - -### Using with Docker Networks - -If your application is also in Docker: - -```yaml -# Your app's docker-compose.yml -services: - your-app: - environment: - LDAP_URL: "ldap://ldap-server:389" # Use container name and internal port - networks: - - ldap_docker_ldap-network # Connect to LDAP network - -networks: - ldap_docker_ldap-network: - external: true -``` - -## Quick Reference - -```bash -# View current configuration -cat .env | grep PORT - -# Generate ldapsearch commands -./scripts/ldapsearch_helper.sh - -# Test connection on custom port -make test-connection - -# Check what ports containers are using -docker-compose ps - -# Change ports (example) -echo "LDAP_PORT=12389" > .env -echo "LDAPS_PORT=12636" >> .env -make down && make start - -# Verify new ports work -make test-users -``` - -## Summary - -**To use custom ports:** -1. Edit `LDAP_PORT` and `LDAPS_PORT` in `.env` -2. Run `make down && make start` -3. All tools automatically use your new ports! - -**Need ldapsearch commands?** -- Run `./scripts/ldapsearch_helper.sh` -- Or manually replace 389 → your LDAP_PORT, 636 → your LDAPS_PORT - -That's it! The port configuration system is designed to be simple and automatic. - ---- - -**See Also:** -- [README.md](README.md) - Full documentation -- [GETTING_STARTED.md](GETTING_STARTED.md) - Setup guide -- [QUICKREF.md](QUICKREF.md) - Command reference \ No newline at end of file diff --git a/QUICKREF.md b/QUICKREF.md deleted file mode 100644 index 6d6ad63..0000000 --- a/QUICKREF.md +++ /dev/null @@ -1,354 +0,0 @@ -# LDAP Docker Quick Reference - -Quick reference for common operations and configurations. - -## 🚀 Quick Start - -```bash -# Complete setup -make dev-setup - -# Or step by step -make install # Install dependencies -make certs-generate # Generate certificates -make start # Start server -make test-connection # Test it works -``` - -## 🎯 Common Commands - -### Server Management - -```bash -make start # Start LDAP server -make stop # Stop LDAP server -make restart # Restart LDAP server -make down # Stop and remove containers -make logs # View logs (follow mode) -make status # Check container status -``` - -### Testing - -```bash -make test-connection # Test LDAP connection -make test-auth # Test authentication -make test-users # List all users -make test-all # Run all tests -``` - -### Certificates - -```bash -make certs-generate # Generate self-signed certs -make certs-check # Verify certificates -``` - -## 🔑 Default Credentials - -### Admin Access - -- **DN:** `cn=admin,dc=testing,dc=local` -- **Password:** `admin_password` -- **Base DN:** `dc=testing,dc=local` - -### phpLDAPadmin - -- URL: http://localhost:8080 -- Login DN: `cn=admin,dc=testing,dc=local` -- Password: `admin_password` - -## 👥 Test Users - -All users have password: `password123` - -| Username | Full Name | Email | DN | -|----------|-----------|-------|-----| -| `admin` | Admin User | admin@testing.local | `uid=admin,ou=people,dc=testing,dc=local` | -| `jdoe` | John Doe | jdoe@testing.local | `uid=jdoe,ou=people,dc=testing,dc=local` | -| `jsmith` | Jane Smith | jsmith@testing.local | `uid=jsmith,ou=people,dc=testing,dc=local` | -| `testuser` | Test User | testuser@testing.local | `uid=testuser,ou=people,dc=testing,dc=local` | - -## 🌐 Service Ports - -| Service | Port | URL/Connection | -|---------|------|----------------| -| LDAP | 389 | `ldap://localhost:389` | -| LDAPS | 636 | `ldaps://localhost:636` | -| phpLDAPadmin | 8080 | `http://localhost:8080` | - -## 🔍 Common LDAP Queries - -### Search All Users - -```bash -ldapsearch -H ldap://localhost:389 \ - -D "cn=admin,dc=testing,dc=local" \ - -w admin_password \ - -b "ou=people,dc=testing,dc=local" \ - "(objectClass=inetOrgPerson)" -``` - -### Search Specific User - -```bash -ldapsearch -H ldap://localhost:389 \ - -D "cn=admin,dc=testing,dc=local" \ - -w admin_password \ - -b "dc=testing,dc=local" \ - "(uid=jdoe)" -``` - -### Search All Groups - -```bash -ldapsearch -H ldap://localhost:389 \ - -D "cn=admin,dc=testing,dc=local" \ - -w admin_password \ - -b "ou=groups,dc=testing,dc=local" \ - "(objectClass=groupOfNames)" -``` - -### Anonymous Bind (Read-Only) - -```bash -ldapsearch -H ldap://localhost:389 \ - -x \ - -b "dc=testing,dc=local" \ - "(objectClass=*)" -``` - -## 🔒 LDAPS/SSL Testing - -### Test SSL Connection - -```bash -openssl s_client -connect localhost:636 -CAfile certs/ca.crt -``` - -### LDAPS Search - -```bash -ldapsearch -H ldaps://localhost:636 \ - -D "cn=admin,dc=testing,dc=local" \ - -w admin_password \ - -b "dc=testing,dc=local" -``` - -### Verify Certificate - -```bash -openssl verify -CAfile certs/ca.crt certs/server.crt -openssl x509 -in certs/server.crt -text -noout -``` - -## 🐍 Python LDAP3 Examples - -### Simple Connection - -```python -from ldap3 import Server, Connection - -server = Server('ldap://localhost:389') -conn = Connection(server, - user='cn=admin,dc=testing,dc=local', - password='admin_password', - auto_bind=True) -print("Connected!") -conn.unbind() -``` - -### Search Users - -```python -from ldap3 import Server, Connection - -server = Server('ldap://localhost:389') -conn = Connection(server, - user='cn=admin,dc=testing,dc=local', - password='admin_password', - auto_bind=True) - -conn.search('dc=testing,dc=local', - '(objectClass=inetOrgPerson)', - attributes=['uid', 'cn', 'mail']) - -for entry in conn.entries: - print(f"{entry.cn}: {entry.mail}") - -conn.unbind() -``` - -### Authenticate User - -```python -from ldap3 import Server, Connection - -server = Server('ldap://localhost:389') -conn = Connection(server, - user='uid=jdoe,ou=people,dc=testing,dc=local', - password='password123') - -if conn.bind(): - print("Authentication successful!") -else: - print("Authentication failed!") -conn.unbind() -``` - -## 🐳 Docker Commands - -### View Logs - -```bash -docker-compose logs -f openldap # Follow LDAP logs -docker-compose logs --tail=100 openldap # Last 100 lines -docker-compose logs phpldapadmin # Admin UI logs -``` - -### Container Shell Access - -```bash -docker-compose exec openldap bash # Shell in LDAP container -docker ps # List running containers -docker-compose ps # List project containers -``` - -### Volume Management - -```bash -docker volume ls # List volumes -docker-compose down -v # Remove volumes (deletes data!) -``` - -## 🔧 Troubleshooting Quick Fixes - -### Server Won't Start - -```bash -# Check if ports are in use -lsof -i :389 -lsof -i :636 -lsof -i :8080 - -# Check Docker is running -docker version - -# View error logs -docker-compose logs openldap -``` - -### Certificate Errors - -```bash -# Verify certificates exist -ls -la certs/ - -# Regenerate certificates -make certs-generate --force - -# Check certificate validity -openssl x509 -in certs/server.crt -noout -dates -``` - -### Connection Refused - -```bash -# Check container is running -docker-compose ps - -# Wait for initialization (can take 10-30 seconds) -make logs - -# Restart server -make restart -``` - -### Authentication Fails - -```bash -# Verify credentials -# Default: cn=admin,dc=testing,dc=local / admin_password - -# Check if users are loaded -make test-users - -# View LDAP directory structure -ldapsearch -H ldap://localhost:389 -x -b "dc=testing,dc=local" -s base -``` - -### Data Not Appearing - -```bash -# Check if LDIF files were loaded -docker-compose logs openldap | grep -i ldif - -# Rebuild with fresh data -make down-volumes # WARNING: Deletes all data! -make start -``` - -## 📁 File Locations - -### Configuration Files - -- `docker-compose.yml` - Docker services configuration -- `pyproject.toml` - Python dependencies -- `.env.example` - Environment variables template - -### Data Files - -- `ldif/01-users.ldif` - Initial LDAP data -- `certs/` - SSL certificates (git-ignored) - -### Scripts - -- `scripts/cli.py` - CLI management tool -- `scripts/generate_certs.py` - Certificate generator -- `quickstart.sh` - Interactive setup script - -## 🎓 LDAP Basics - -### DN (Distinguished Name) - -Format: `attribute=value,ou=unit,dc=domain,dc=tld` - -Examples: -- `cn=admin,dc=testing,dc=local` - Admin user -- `uid=jdoe,ou=people,dc=testing,dc=local` - Regular user -- `cn=developers,ou=groups,dc=testing,dc=local` - Group - -### Common Object Classes - -- `inetOrgPerson` - Person with internet attributes -- `posixAccount` - Unix/Linux account -- `groupOfNames` - Group with members - -### Common Attributes - -- `uid` - User ID (username) -- `cn` - Common Name (full name) -- `sn` - Surname (last name) -- `mail` - Email address -- `userPassword` - Hashed password -- `member` - Group member DN - -## 🔗 Useful Links - -- [OpenLDAP Documentation](https://www.openldap.org/doc/) -- [LDAP3 Python Library](https://ldap3.readthedocs.io/) -- [RFC 4511 - LDAP Protocol](https://tools.ietf.org/html/rfc4511) -- [phpLDAPadmin](http://phpldapadmin.sourceforge.net/) - -## 💡 Tips - -1. **Use LDAPS in applications**: Always prefer `ldaps://` over `ldap://` -2. **Test with anonymous bind first**: Use `-x` flag with ldapsearch -3. **Check logs when troubleshooting**: `make logs` is your friend -4. **Certificate hostname must match**: Ensure SAN includes `ldap.testing.local` -5. **Wait after starting**: Server needs 10-30 seconds to initialize -6. **Backup before experimenting**: Use `make down` not `make down-volumes` - ---- - -**Need more help?** See full documentation in README.md \ No newline at end of file diff --git a/README.md b/README.md index e4a0513..6dafd5a 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,31 @@ # LDAP Docker -This is a development tool project that, when deployed, offers an SSL-capable OpenLDAP server populated with test users for a fictional `testing.local` domain. +A pre-configured OpenLDAP server with SSL/TLS support and test users for the `{.env:LDAP_DOMAIN}` domain. -Perfect for local development and testing of applications that need LDAP authentication without setting up a complex infrastructure. +Perfect for local development and testing of applications that need LDAP authentication without setting up a complex infrastructure. Just run `docker-compose up -d` and you're ready to go! ## Features -- 🔒 **SSL/TLS Support** - LDAPS on port 636 with custom certificate support -- 👥 **Pre-populated Users** - Test users and groups ready to use -- 🌐 **Web Admin Interface** - phpLDAPadmin for easy management -- 🐳 **Docker-based** - Easy deployment with Docker Compose -- 🔧 **Python Management Tools** - UV-based CLI for convenience -- 🍎 **Cross-platform** - Works on MacOS (Rancher Desktop), Linux, and Windows +- **SSL/TLS Support** - LDAPS on port `{.env:LDAPS_PORT}` with custom certificate support +- **Pre-populated Users** - Test users and groups ready to use +- **Web Admin Interface** - phpLDAPadmin for easy management +- **Docker-based** - Easy deployment with Docker Compose +- **Fully Configurable** - Customize ports, domain, credentials via `.env` file +- **Optional Management Tools** - Makefile and Python scripts for convenience +- **Cross-platform** - Works anywhere Docker runs + +> **📝 Note:** Throughout this documentation, values shown as `{.env:VARIABLE_NAME}` indicate they can be customized via environment variables. See [Configuration](#configuration) for details. ## Table of Contents - [Prerequisites](#prerequisites) - [Quick Start](#quick-start) +- [Verify Installation](#verify-installation) - [Custom Certificates (Dev-CA)](#custom-certificates-dev-ca) - [Usage](#usage) +- [Configuration](#configuration) - [Test Users](#test-users) +- [Next Steps](#next-steps) - [Management Tools](#management-tools) - [Project Structure](#project-structure) - [Troubleshooting](#troubleshooting) @@ -29,67 +35,122 @@ Perfect for local development and testing of applications that need LDAP authent ### Required -- **Docker** or **Rancher Desktop** (recommended for MacOS) - - MacOS: [Download Rancher Desktop](https://rancherdesktop.io/) - - Alternatively: [Docker Desktop](https://www.docker.com/products/docker-desktop) -- **Python 3.9+** (for management scripts) -- **UV Package Manager** (recommended) +- **Docker** - Any provider that can run Docker Compose + - [Docker Desktop](https://www.docker.com/products/docker-desktop) (Windows, Mac, Linux) + - [Rancher Desktop](https://rancherdesktop.io/) (Alternative for Mac/Linux) + - Or native Docker on Linux + +- **SSL Certificates** - Either: + - Generate with the included Python script (requires Python 3.9+) + - OR bring your own certificates (see [Custom Certificates](#custom-certificates-dev-ca)) + +That's it! Everything runs in Docker containers. + +### Optional Tools + +These tools provide convenient shortcuts but are **not required**: + +- **Make** - For using `make start` instead of `docker-compose up -d` +- **Python 3.9+** - For certificate generation script and examples +- **UV Package Manager** - Fast Python dependency management ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` - -### Optional - -- **OpenSSL** (for certificate verification) -- **LDAP utilities** (ldapsearch, ldapadd, etc.) for testing +- **OpenSSL** - For manual certificate verification +- **LDAP utilities** - Command-line tools (ldapsearch, ldapadd, etc.) ## Quick Start -### Option 1: Using Make (Recommended) +### Simple Way (Docker Only) ```bash -# 1. Install dependencies -make install +# 1. Generate certificates (requires Python) +python3 scripts/generate_certs.py -# 2. Generate certificates (or copy your own - see below) -make certs-generate - -# 3. Start the server -make start - -# 4. Test the connection -make test-connection - -# 5. List users -make test-users -``` - -### Option 2: Using Docker Compose Directly - -```bash -# 1. Generate certificates -python scripts/generate_certs.py - -# 2. Start services +# 2. Start the server docker-compose up -d # 3. View logs docker-compose logs -f openldap + +# 4. Stop the server +docker-compose down ``` -### Option 3: Complete Dev Setup +That's it! The server is now running at `ldap://localhost:{.env:LDAP_PORT}` and `ldaps://localhost:{.env:LDAPS_PORT}`. -This will install everything, generate certificates, and start the server: +### Convenient Way (Using Make) + +If you have Make installed, use these shortcuts: ```bash +# Complete setup (installs Python deps, generates certs, starts server) make dev-setup + +# Or step by step: +make install # Install Python dependencies +make certs-generate # Generate SSL certificates +make start # Start the server +make verify-users # Verify users are loaded +make logs # View logs ``` +Run `make help` to see all available commands. + +### Using Your Own Certificates + +Instead of generating certificates, copy yours to the `certs/` directory: + +```bash +cp /path/to/ca.crt certs/ca.crt +cp /path/to/server.crt certs/server.crt +cp /path/to/server.key certs/server.key +docker-compose up -d +``` + +> **💡 Tip:** Want to customize ports, passwords, or domain? Copy `.env.example` to `.env` and edit your settings. See the [Configuration](#configuration) section for details. + +## Verify Installation + +After starting the server, verify everything is working: + +### Test 1: List All Users + +```bash +make verify-users +``` + +You should see output like: + +``` +Found 4 user(s): + + - Admin User: admin (admin@{.env:LDAP_DOMAIN}) + - John Doe: jdoe (jdoe@{.env:LDAP_DOMAIN}) + - Jane Smith: jsmith (jsmith@{.env:LDAP_DOMAIN}) + - Test User: testuser (testuser@{.env:LDAP_DOMAIN}) +``` + +### Test 2: Try the Web Interface + +Open `http://localhost:{.env:PHPLDAPADMIN_PORT}` in your browser and login with: + +- **Login DN:** `cn=admin,{.env:LDAP_BASE_DN}` +- **Password:** `{.env:LDAP_ADMIN_PASSWORD}` + +### Test 3: Run the Example Script (Optional - Requires Python) + +```bash +python examples/simple_auth.py +``` + +This authenticates user `jdoe` and displays their information. + ## Custom Certificates (Dev-CA) -If you maintain your own dev-ca (as mentioned), you can use your own certificates instead of self-signed ones: +If you wish, you can use your own certificates instead of self-signed ones: -### Using Your Dev-CA Certificates +### Using Your Certificates ```bash # Copy your certificates to the certs directory @@ -103,15 +164,15 @@ chmod 644 certs/server.crt chmod 600 certs/server.key ``` -**Important:** The server certificate should be issued for the hostname `ldap.testing.local` or include it as a Subject Alternative Name (SAN). +**Important:** The server certificate should be issued for the hostname `{.env:LDAP_HOSTNAME}` or include it as a Subject Alternative Name (SAN). ### Certificate Requirements The OpenLDAP container expects three files in the `certs/` directory: -- `ca.crt` - Your CA root certificate -- `server.crt` - Server certificate for ldap.testing.local -- `server.key` - Private key for the server certificate +- `ca.crt` - Your CA root certificate (filename: `{.env:LDAP_TLS_CA_CRT_FILENAME}`) +- `server.crt` - Server certificate for `{.env:LDAP_HOSTNAME}` (filename: `{.env:LDAP_TLS_CRT_FILENAME}`) +- `server.key` - Private key for the server certificate (filename: `{.env:LDAP_TLS_KEY_FILENAME}`) ### Generating Certificates with Your OpenSSL-based Dev-CA @@ -124,16 +185,16 @@ cd /path/to/your/dev-ca # Generate server key openssl genrsa -out ldap-server.key 4096 -# Generate certificate signing request +# Generate certificate signing request (use your LDAP_HOSTNAME value) openssl req -new -key ldap-server.key -out ldap-server.csr \ - -subj "/CN=ldap.testing.local" + -subj "/CN={.env:LDAP_HOSTNAME}" # Sign with your CA (adjust paths as needed) openssl x509 -req -in ldap-server.csr \ -CA ca-cert.pem -CAkey ca-key.pem \ -CAcreateserial -out ldap-server.crt \ -days 365 -sha256 \ - -extfile <(printf "subjectAltName=DNS:ldap.testing.local,DNS:localhost,IP:127.0.0.1") + -extfile <(printf "subjectAltName=DNS:{.env:LDAP_HOSTNAME},DNS:localhost,IP:127.0.0.1") # Copy to LDAP Docker project cp ldap-server.crt /path/to/ldap_docker/certs/server.crt @@ -147,17 +208,19 @@ cp ca-cert.pem /path/to/ldap_docker/certs/ca.crt Once started, the following services are available: -| Service | URL/Connection | Description | -|---------|----------------|-------------| -| LDAP | `ldap://localhost:389` | Standard LDAP (unencrypted) | -| LDAPS | `ldaps://localhost:636` | LDAP over SSL/TLS | -| phpLDAPadmin | `http://localhost:8080` | Web-based administration | +| Service | URL/Connection | Description | +| ------------ | ----------------------- | --------------------------- | +| LDAP | `ldap://localhost:{.env:LDAP_PORT}` | Standard LDAP (unencrypted) | +| LDAPS | `ldaps://localhost:{.env:LDAPS_PORT}` | LDAP over SSL/TLS | +| phpLDAPadmin | `http://localhost:{.env:PHPLDAPADMIN_PORT}` | Web-based administration | ### Admin Credentials -- **Admin DN:** `cn=admin,dc=testing,dc=local` -- **Password:** `admin_password` -- **Base DN:** `dc=testing,dc=local` +- **Admin DN:** `cn=admin,{.env:LDAP_BASE_DN}` +- **Password:** `{.env:LDAP_ADMIN_PASSWORD}` +- **Base DN:** `{.env:LDAP_BASE_DN}` + +> **Note:** Default values shown. Customize these by creating a `.env` file (see [Configuration](#configuration)). ### Common Commands @@ -174,8 +237,11 @@ make logs # Check status make status -# Run tests -make test-all +# Run unit tests +make test + +# Verify running server +make verify-all # Open shell in container make shell @@ -184,16 +250,119 @@ make shell make clean-all ``` -## Test Users +## Configuration + +You can customize the LDAP server behavior using environment variables. The easiest way is to create a `.env` file in the project root. + +### Quick Configuration + +```bash +# Copy the example configuration +cp .env.example .env + +# Edit with your preferred settings +nano .env # or vim, code, etc. +``` + +### Available Configuration Options + +The `.env.example` file includes all available options with documentation. Below is a comprehensive reference of all environment variables: + +| Variable | Default Value | Description | Usage Reference | +|----------|---------------|-------------|-----------------| +| **Domain & Organization** |||| +| `LDAP_DOMAIN` | `testing.local` | Your LDAP domain | Used in: [Services](#accessing-the-services), [Test Users](#default-test-users) | +| `LDAP_ORGANISATION` | `Testing Organization` | Organization name | Used in LDAP server metadata | +| `LDAP_BASE_DN` | `dc=testing,dc=local` | Base Distinguished Name (auto-derived from domain) | Used in: [Services](#accessing-the-services), [Admin Credentials](#admin-credentials), [Test Users](#default-test-users) | +| **Credentials** |||| +| `LDAP_ADMIN_PASSWORD` | `admin_password` | Admin user password | Used in: [Admin Credentials](#admin-credentials), [Verify Installation](#verify-installation) | +| `LDAP_CONFIG_PASSWORD` | `config_password` | Configuration admin password | Used for LDAP server configuration | +| **Ports** |||| +| `LDAP_PORT` | `389` | Standard LDAP port (unencrypted) | Used in: [Services](#accessing-the-services), [Testing Authentication](#testing-authentication) | +| `LDAPS_PORT` | `636` | LDAPS/SSL port (encrypted) | Used in: [Services](#accessing-the-services), [Testing Authentication](#testing-authentication) | +| `PHPLDAPADMIN_PORT` | `8080` | Web UI port | Used in: [Services](#accessing-the-services), [Verify Installation](#verify-installation) | +| **SSL/TLS** |||| +| `LDAP_TLS` | `true` | Enable TLS support | Controls SSL/TLS functionality | +| `LDAP_TLS_CRT_FILENAME` | `server.crt` | Server certificate filename | Used in: [Custom Certificates](#custom-certificates-dev-ca) | +| `LDAP_TLS_KEY_FILENAME` | `server.key` | Server private key filename | Used in: [Custom Certificates](#custom-certificates-dev-ca) | +| `LDAP_TLS_CA_CRT_FILENAME` | `ca.crt` | CA certificate filename | Used in: [Custom Certificates](#custom-certificates-dev-ca) | +| `LDAP_TLS_VERIFY_CLIENT` | `try` | Client certificate verification mode | Options: `never`, `allow`, `try`, `demand` | +| **Container Configuration** |||| +| `LDAP_HOSTNAME` | `ldap.testing.local` | LDAP server hostname | Used in certificate validation | +| `LDAP_CONTAINER_NAME` | `ldap-server` | LDAP container name | Used in Docker commands | +| `PHPLDAPADMIN_CONTAINER_NAME` | `ldap-admin` | phpLDAPadmin container name | Used in Docker commands | +| **Other Options** |||| +| `LDAP_LOG_LEVEL` | `256` | Logging verbosity | Higher = more verbose | + +> **Tip:** Throughout this documentation, values shown as `{.env:VARIABLE_NAME}` indicate they are configured via these environment variables. + +#### Summary by Category + +##### Domain & Organization +- `LDAP_DOMAIN` - Your LDAP domain (default: `testing.local`) +- `LDAP_ORGANISATION` - Organization name (default: `Testing Organization`) +- `LDAP_BASE_DN` - Base DN, auto-derived from domain if not set + +##### Credentials +- `LDAP_ADMIN_PASSWORD` - Admin password (default: `admin_password`) +- `LDAP_CONFIG_PASSWORD` - Config admin password (default: `config_password`) + +##### Ports +- `LDAP_PORT` - Standard LDAP port (default: `389`) +- `LDAPS_PORT` - LDAPS/SSL port (default: `636`) +- `PHPLDAPADMIN_PORT` - Web UI port (default: `8080`) + +##### SSL/TLS +- `LDAP_TLS` - Enable TLS (default: `true`) +- `LDAP_TLS_CRT_FILENAME` - Server cert filename (default: `server.crt`) +- `LDAP_TLS_KEY_FILENAME` - Server key filename (default: `server.key`) +- `LDAP_TLS_CA_CRT_FILENAME` - CA cert filename (default: `ca.crt`) + +##### Container Names +- `LDAP_HOSTNAME` - LDAP server hostname (default: `ldap.testing.local`) +- `LDAP_CONTAINER_NAME` - Container name (default: `ldap-server`) +- `PHPLDAPADMIN_CONTAINER_NAME` - Admin UI container (default: `ldap-admin`) + +##### Other Options +- `LDAP_LOG_LEVEL` - Logging verbosity (default: `256`) +- `DEBUG` - Enable debug output (default: `false`) +- `TZ` - Timezone (default: `UTC`) + +### Example Custom Configuration + +```bash +# .env +LDAP_DOMAIN=mycompany.local +LDAP_ORGANISATION=My Company +LDAP_ADMIN_PASSWORD=mysecurepassword + +# Use different ports +LDAP_PORT=1389 +LDAPS_PORT=1636 +PHPLDAPADMIN_PORT=9080 +``` + +After creating or modifying `.env`, restart the containers: + +```bash +docker-compose down +docker-compose up -d +``` + +For a complete list of options with detailed descriptions, see `.env.example`. + +## Default Test Users The LDAP directory is pre-populated with the following test users: -| Username | Full Name | Email | Password | UID | -|----------|-----------|-------|----------|-----| -| admin | Admin User | admin@testing.local | password123 | 10000 | -| jdoe | John Doe | jdoe@testing.local | password123 | 10001 | -| jsmith | Jane Smith | jsmith@testing.local | password123 | 10002 | -| testuser | Test User | testuser@testing.local | password123 | 10003 | +| Username | Full Name | Email | Password | UID | +| -------- | ---------- | -------------------------------- | ----------- | ----- | +| admin | Admin User | admin@`{.env:LDAP_DOMAIN}` | password123 | 10000 | +| jdoe | John Doe | jdoe@`{.env:LDAP_DOMAIN}` | password123 | 10001 | +| jsmith | Jane Smith | jsmith@`{.env:LDAP_DOMAIN}` | password123 | 10002 | +| testuser | Test User | testuser@`{.env:LDAP_DOMAIN}` | password123 | 10003 | + +> **Note:** Email addresses are based on `{.env:LDAP_DOMAIN}` (default: `testing.local`). User DNs follow the pattern `uid=username,ou=people,{.env:LDAP_BASE_DN}`. ### Test Groups @@ -204,57 +373,126 @@ The LDAP directory is pre-populated with the following test users: ### Testing Authentication ```bash -# Test with ldapsearch -ldapsearch -H ldap://localhost:389 \ - -D "uid=jdoe,ou=people,dc=testing,dc=local" \ +# Test with ldapsearch (using default ports from .env) +ldapsearch -H ldap://localhost:{.env:LDAP_PORT} \ + -D "uid=jdoe,ou=people,{.env:LDAP_BASE_DN}" \ -w password123 \ - -b "dc=testing,dc=local" \ + -b "{.env:LDAP_BASE_DN}" \ "(uid=jdoe)" # Test LDAPS with SSL -ldapsearch -H ldaps://localhost:636 \ - -D "uid=jdoe,ou=people,dc=testing,dc=local" \ +ldapsearch -H ldaps://localhost:{.env:LDAPS_PORT} \ + -D "uid=jdoe,ou=people,{.env:LDAP_BASE_DN}" \ -w password123 \ - -b "dc=testing,dc=local" \ + -b "{.env:LDAP_BASE_DN}" \ "(uid=jdoe)" + +# Best practice: Test auth by searching for user's own entry +# This works for all users, not just admin +ldapsearch -H ldap://localhost:{.env:LDAP_PORT} \ + -D "uid=jdoe,ou=people,{.env:LDAP_BASE_DN}" \ + -w password123 \ + -b "uid=jdoe,ou=people,{.env:LDAP_BASE_DN}" \ + -s base \ + "(objectClass=*)" ``` +## Next Steps + +Now that your LDAP server is running, you can: + +### 1. Integrate with Your Application + +Point your application to the LDAP server: + +- **LDAP URL:** `ldap://localhost:{.env:LDAP_PORT}` +- **LDAPS URL:** `ldaps://localhost:{.env:LDAPS_PORT}` +- **Base DN:** `{.env:LDAP_BASE_DN}` + +See `examples/README.md` for code samples and integration patterns. + +### 2. Add Custom Users + +Edit `ldif/01-users.ldif` to add more users or modify existing ones, then reload: + +```bash +make down-volumes # WARNING: Deletes all data +make start +``` + +### 3. Customize Configuration + +Create a `.env` file to customize ports, credentials, and behavior: + +```bash +cp .env.example .env +# Edit .env with your settings +docker-compose down && docker-compose up -d +``` + +See the [Configuration](#configuration) section for all available options. + +### 4. Learn More + +- `certs/README.md` - Certificate management guide +- `examples/README.md` - Integration examples and patterns +- [OpenLDAP Documentation](https://www.openldap.org/doc/) + ## Management Tools -This project includes Python-based management tools using UV. +You can manage the LDAP server using `docker-compose` commands directly, or use the included Makefile for convenience. -### Installation +### Using Docker Compose (No Additional Tools Required) ```bash -# Install dependencies with UV -make install +# Server management +docker-compose up -d # Start server +docker-compose stop # Stop server +docker-compose restart # Restart server +docker-compose logs -f openldap # View logs +docker-compose ps # Check status +docker-compose down # Remove containers +docker-compose down -v # Remove containers and data -# Or manually -uv sync +# Generate certificates (requires Python) +python3 scripts/generate_certs.py ``` -### CLI Tool Usage +### Using Make (Optional Convenience) + +If you have Make installed, use these shortcuts: ```bash -# View available commands -uv run ldap-docker --help +make help # View all available commands # Server management -uv run ldap-docker server start -uv run ldap-docker server stop -uv run ldap-docker server logs -f +make start # Start the LDAP server +make stop # Stop the LDAP server +make restart # Restart the LDAP server +make logs # View server logs +make status # Check server status # Certificate management -uv run ldap-docker certs generate -uv run ldap-docker certs check +make certs-generate # Generate SSL certificates +make certs-check # Verify SSL certificates -# Testing -uv run ldap-docker test connection -uv run ldap-docker test auth -uv run ldap-docker test users +# Unit Testing (requires Python + pytest) +make test # Run unit tests +make test-cov # Run tests with coverage -# Initialize environment -uv run ldap-docker init +# Verification (requires running container + ldap3) +make verify-connection # Verify LDAP connection +make verify-auth # Verify authentication +make verify-users # List all users +make verify-all # Run all verification checks + +# Setup +make install # Install Python dependencies +make dev-setup # Complete development setup + +# Cleanup +make clean # Clean build artifacts +make down-volumes # Remove containers and data ``` ## Project Structure @@ -269,7 +507,6 @@ ldap_docker/ ├── ldif/ # LDAP Data Interchange Format files │ └── 01-users.ldif # Initial user and group data ├── scripts/ # Management scripts -│ ├── cli.py # CLI tool for managing LDAP │ └── generate_certs.py # Certificate generation utility ├── docker-compose.yml # Docker Compose configuration ├── pyproject.toml # Python project configuration (UV) @@ -332,14 +569,14 @@ docker-compose ps make logs # Test basic connectivity -telnet localhost 389 +telnet localhost {.env:LDAP_PORT} ``` **Problem:** Authentication fails ```bash # Verify credentials -# Default admin: cn=admin,dc=testing,dc=local / admin_password +# Default admin: cn=admin,{.env:LDAP_BASE_DN} / {.env:LDAP_ADMIN_PASSWORD} # Check LDAP logs docker-compose logs openldap | grep -i error @@ -365,14 +602,14 @@ make start Edit `ldif/01-users.ldif` to add more users or modify existing ones: ```ldif -dn: uid=newuser,ou=people,dc=testing,dc=local +dn: uid=newuser,ou=people,{.env:LDAP_BASE_DN} objectClass: inetOrgPerson objectClass: posixAccount objectClass: shadowAccount uid: newuser cn: New User sn: User -mail: newuser@testing.local +mail: newuser@{.env:LDAP_DOMAIN} userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ= uidNumber: 10004 gidNumber: 10004 @@ -380,6 +617,8 @@ homeDirectory: /home/newuser loginShell: /bin/bash ``` +> **Note:** Replace `{.env:LDAP_BASE_DN}` and `{.env:LDAP_DOMAIN}` with your actual values from the `.env` file (defaults: `dc=testing,dc=local` and `testing.local`). + Then restart with fresh data: ```bash @@ -389,39 +628,43 @@ make start ### Modifying Configuration -Edit `docker-compose.yml` to change: -- Port mappings -- Environment variables -- Volume mounts -- Resource limits - -### Python Development +The easiest way to customize settings is with a `.env` file: ```bash -# Install development dependencies +cp .env.example .env +# Edit .env with your preferences +docker-compose down && docker-compose up -d +``` + +See `.env.example` for all available options with detailed documentation, or the [Configuration](#configuration) section. + +For advanced customization, you can also edit `docker-compose.yml` to change volume mounts, resource limits, or other Docker-specific settings. + +### Python Development (Optional) + +If you're contributing to the Python scripts: + +```bash +# Install development dependencies (requires UV) make install-dev # Run tests uv run pytest -# Format code -uv run black scripts/ - -# Lint code -uv run ruff check scripts/ - -# Type check -uv run mypy scripts/ +# Format and lint code +ruff format scripts/ +ruff check scripts/ ``` -### Building on Other Platforms +### Cross-Platform Compatibility -This project is designed to work on: -- **MacOS** (with Rancher Desktop or Docker Desktop) -- **Linux** (with Docker and Docker Compose) -- **Windows** (with Docker Desktop and WSL2) +This project uses standard Docker images and docker-compose.yml format, so it works anywhere Docker runs: -The `docker-compose.yml` uses standard Docker images and should work anywhere Docker runs. +- **Linux** - Native Docker +- **MacOS** - Docker Desktop, Rancher Desktop, or Colima +- **Windows** - Docker Desktop (with WSL2) + +No platform-specific code or configuration needed! ## Security Notes @@ -442,6 +685,7 @@ MIT License - See LICENSE file for details ## Contributing Contributions are welcome! This is a development tool, so: + 1. Keep it simple and easy to use 2. Maintain cross-platform compatibility 3. Update documentation for any changes diff --git a/certs/README.md b/certs/README.md index a9f4436..98d7982 100644 --- a/certs/README.md +++ b/certs/README.md @@ -6,13 +6,13 @@ This directory should contain your SSL/TLS certificates for the LDAP server. The OpenLDAP container expects the following files in this directory: -- `ca.crt` - Certificate Authority certificate (your dev-ca root certificate) -- `server.crt` - Server certificate for ldap.testing.local -- `server.key` - Private key for the server certificate +- `ca.crt` - Certificate Authority certificate (your dev-ca root certificate) - filename: `{.env:LDAP_TLS_CA_CRT_FILENAME}` +- `server.crt` - Server certificate for `{.env:LDAP_HOSTNAME}` - filename: `{.env:LDAP_TLS_CRT_FILENAME}` +- `server.key` - Private key for the server certificate - filename: `{.env:LDAP_TLS_KEY_FILENAME}` ## Using Your Custom Dev-CA Certificates -If you maintain your own dev-ca (as mentioned), simply copy your certificates here: +If you maintain your own dev-ca, simply copy your certificates here: ```bash # Copy your dev-ca generated certificates to this directory @@ -22,9 +22,9 @@ cp /path/to/your/dev-ca/ca-cert.pem ./ca.crt ``` **Important Notes:** -- The server certificate should be issued for the hostname `ldap.testing.local` +- The server certificate should be issued for the hostname `{.env:LDAP_HOSTNAME}` (default: `ldap.testing.local`) - The certificate can also include SANs (Subject Alternative Names) like: - - `DNS:ldap.testing.local` + - `DNS:{.env:LDAP_HOSTNAME}` - `DNS:localhost` - `IP:127.0.0.1` - Ensure the private key is readable by the container (permissions should be 600 or 644) @@ -46,13 +46,13 @@ openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \ # Generate server private key openssl genrsa -out server.key 4096 -# Generate server certificate signing request +# Generate server certificate signing request (use your LDAP_HOSTNAME value) openssl req -new -key server.key -out server.csr \ - -subj "/C=US/ST=State/L=City/O=Testing Org/CN=ldap.testing.local" + -subj "/C=US/ST=State/L=City/O=Testing Org/CN={.env:LDAP_HOSTNAME}" -# Create extensions file for SAN +# Create extensions file for SAN (use your LDAP_HOSTNAME value) cat > server-ext.cnf < **📝 Note:** In the examples below, values shown as `{.env:VARIABLE_NAME}` are configurable via environment variables in your `.env` file. The actual default values are: `LDAP_PORT=389`, `LDAPS_PORT=636`, `LDAP_BASE_DN=dc=testing,dc=local`, and `LDAP_ADMIN_PASSWORD=admin_password`. See the main [Configuration](../README.md#configuration) section for all available options. + ## Available Examples ### 1. Simple Authentication (`simple_auth.py`) @@ -9,6 +11,7 @@ This directory contains example scripts and applications demonstrating how to us A Python script demonstrating basic LDAP authentication and user information retrieval. **Features:** + - Authenticate users with username/password - Retrieve detailed user information - Get user group memberships @@ -27,14 +30,14 @@ python examples/simple_auth.py --username jsmith --password password123 python examples/simple_auth.py --list-users # Use a different LDAP server -python examples/simple_auth.py --server ldaps://localhost:636 +python examples/simple_auth.py --server ldaps://localhost:{.env:LDAPS_PORT} ``` **Example Output:** ``` -🔐 LDAP Authentication Example -Server: ldap://localhost:389 +LDAP Authentication Example +Server: ldap://localhost:{.env:LDAP_PORT} Attempting to authenticate user: jdoe ✅ Authentication successful for user: jdoe @@ -52,7 +55,7 @@ Last Name: Doe Email: jdoe@testing.local UID Number: 10001 GID Number: 10001 -DN: uid=jdoe,ou=people,dc=testing,dc=local +DN: uid=jdoe,ou=people,{.env:LDAP_BASE_DN} ================================================== Fetching user groups... @@ -69,17 +72,17 @@ User belongs to 2 group(s): from ldap3 import Server, Connection # Connect and authenticate -server = Server('ldap://localhost:389') +server = Server('ldap://localhost:{.env:LDAP_PORT}') conn = Connection( server, - user='uid=jdoe,ou=people,dc=testing,dc=local', + user='uid=jdoe,ou=people,{.env:LDAP_BASE_DN}', password='password123', auto_bind=True ) # Search for users conn.search( - 'dc=testing,dc=local', + '{.env:LDAP_BASE_DN}', '(objectClass=inetOrgPerson)', attributes=['uid', 'cn', 'mail'] ) @@ -94,243 +97,17 @@ conn.unbind() ```bash # Search for a user -ldapsearch -H ldap://localhost:389 \ - -D "cn=admin,dc=testing,dc=local" \ - -w admin_password \ - -b "dc=testing,dc=local" \ +ldapsearch -H ldap://localhost:{.env:LDAP_PORT} \ + -D "cn=admin,{.env:LDAP_BASE_DN}" \ + -w {.env:LDAP_ADMIN_PASSWORD} \ + -b "{.env:LDAP_BASE_DN}" \ "(uid=jdoe)" # List all users -ldapsearch -H ldap://localhost:389 \ - -D "cn=admin,dc=testing,dc=local" \ - -w admin_password \ - -b "ou=people,dc=testing,dc=local" \ +ldapsearch -H ldap://localhost:{.env:LDAP_PORT} \ + -D "cn=admin,{.env:LDAP_BASE_DN}" \ + -w {.env:LDAP_ADMIN_PASSWORD} \ + -b "ou=people,{.env:LDAP_BASE_DN}" \ "(objectClass=inetOrgPerson)" \ uid cn mail ``` - -### Web Application Integration - -#### Flask Example - -```python -from flask import Flask, request, jsonify -from ldap3 import Server, Connection - -app = Flask(__name__) - -@app.route('/login', methods=['POST']) -def login(): - username = request.json.get('username') - password = request.json.get('password') - - server = Server('ldap://localhost:389') - user_dn = f'uid={username},ou=people,dc=testing,dc=local' - - try: - conn = Connection(server, user=user_dn, password=password) - if conn.bind(): - return jsonify({'status': 'success', 'message': 'Authenticated'}) - else: - return jsonify({'status': 'error', 'message': 'Invalid credentials'}), 401 - except: - return jsonify({'status': 'error', 'message': 'Authentication failed'}), 401 -``` - -#### Django Example - -```python -# settings.py -import ldap -from django_auth_ldap.config import LDAPSearch - -AUTH_LDAP_SERVER_URI = "ldap://localhost:389" -AUTH_LDAP_BIND_DN = "cn=admin,dc=testing,dc=local" -AUTH_LDAP_BIND_PASSWORD = "admin_password" -AUTH_LDAP_USER_SEARCH = LDAPSearch( - "ou=people,dc=testing,dc=local", - ldap.SCOPE_SUBTREE, - "(uid=%(user)s)" -) - -AUTHENTICATION_BACKENDS = [ - 'django_auth_ldap.backend.LDAPBackend', - 'django.contrib.auth.backends.ModelBackend', -] -``` - -## Common Integration Patterns - -### 1. Simple Bind Authentication - -The most straightforward approach - try to bind with user credentials: - -```python -def authenticate_user(username, password): - server = Server('ldap://localhost:389') - user_dn = f'uid={username},ou=people,dc=testing,dc=local' - conn = Connection(server, user=user_dn, password=password) - return conn.bind() -``` - -### 2. Search and Bind - -Search for the user first, then authenticate: - -```python -def authenticate_user(username, password): - # First, search for the user with admin credentials - server = Server('ldap://localhost:389') - admin_conn = Connection( - server, - user='cn=admin,dc=testing,dc=local', - password='admin_password', - auto_bind=True - ) - - admin_conn.search( - 'ou=people,dc=testing,dc=local', - f'(uid={username})', - attributes=['dn'] - ) - - if not admin_conn.entries: - return False - - user_dn = admin_conn.entries[0].entry_dn - admin_conn.unbind() - - # Now authenticate as the user - user_conn = Connection(server, user=user_dn, password=password) - return user_conn.bind() -``` - -### 3. Group-Based Authorization - -Check if user belongs to specific groups: - -```python -def user_has_role(username, required_group): - server = Server('ldap://localhost:389') - conn = Connection( - server, - user='cn=admin,dc=testing,dc=local', - password='admin_password', - auto_bind=True - ) - - user_dn = f'uid={username},ou=people,dc=testing,dc=local' - - conn.search( - 'ou=groups,dc=testing,dc=local', - f'(&(objectClass=groupOfNames)(member={user_dn})(cn={required_group}))', - attributes=['cn'] - ) - - return len(conn.entries) > 0 -``` - -## Testing Your Integration - -### 1. Start the LDAP Server - -```bash -make start -``` - -### 2. Test Connection - -```bash -python examples/simple_auth.py --list-users -``` - -### 3. Test Authentication - -```bash -python examples/simple_auth.py --username jdoe --password password123 -``` - -### 4. Test with Your Application - -Point your application to: -- LDAP URL: `ldap://localhost:389` -- LDAPS URL: `ldaps://localhost:636` (with SSL) -- Base DN: `dc=testing,dc=local` - -## Available Test Accounts - -| Username | Password | Groups | Purpose | -|----------|----------|--------|---------| -| admin | password123 | admins | Administrative testing | -| jdoe | password123 | developers, users | Regular user testing | -| jsmith | password123 | developers, users | Regular user testing | -| testuser | password123 | users | Basic user testing | - -## SSL/TLS Configuration - -For production-like testing with LDAPS: - -```python -import ssl -from ldap3 import Server, Connection, Tls - -tls = Tls( - ca_certs_file='certs/ca.crt', - validate=ssl.CERT_REQUIRED -) - -server = Server('ldaps://localhost:636', use_ssl=True, tls=tls) -conn = Connection(server, user=user_dn, password=password, auto_bind=True) -``` - -## Troubleshooting - -### Connection Refused - -```bash -# Check if LDAP server is running -make status - -# Start if not running -make start -``` - -### Authentication Fails - -```bash -# Verify user exists -make test-users - -# Check LDAP logs -make logs -``` - -### Python ImportError - -```bash -# Install ldap3 library -uv pip install ldap3 -# or -pip install ldap3 -``` - -## Additional Resources - -- [ldap3 Documentation](https://ldap3.readthedocs.io/) -- [LDAP Protocol Overview](https://ldap.com/ldap-protocol/) -- [Django LDAP Authentication](https://django-auth-ldap.readthedocs.io/) -- [Flask-LDAP3-Login](https://flask-ldap3-login.readthedocs.io/) - -## Contributing Examples - -Have an example for a specific framework or use case? Contributions are welcome! - -Examples we'd love to see: -- Express.js / Node.js authentication -- Ruby on Rails integration -- Go LDAP client -- Java Spring Security LDAP -- PHP authentication -- Docker Compose with application stack - -Submit a pull request with your example! \ No newline at end of file diff --git a/examples/simple_auth.py b/examples/simple_auth.py index 4fa15a1..7721278 100644 --- a/examples/simple_auth.py +++ b/examples/simple_auth.py @@ -13,11 +13,10 @@ Usage: import argparse import os import sys -from typing import Dict, List, Optional try: from ldap3 import ALL, Connection, Server - from ldap3.core.exceptions import LDAPException, LDAPBindError + from ldap3.core.exceptions import LDAPBindError, LDAPException except ImportError: print("Error: ldap3 library not found.") print("Install it with: uv pip install ldap3") @@ -27,9 +26,9 @@ except ImportError: # Configuration LDAP_PORT = os.environ.get("LDAP_PORT", "389") LDAP_SERVER = f"ldap://localhost:{LDAP_PORT}" -LDAP_BASE_DN = "dc=testing,dc=local" -LDAP_PEOPLE_OU = "ou=people,dc=testing,dc=local" -LDAP_GROUPS_OU = "ou=groups,dc=testing,dc=local" +LDAP_BASE_DN = os.environ.get("LDAP_BASE_DN", "dc=testing,dc=local") +LDAP_PEOPLE_OU = f"ou=people,{LDAP_BASE_DN}" +LDAP_GROUPS_OU = f"ou=groups,{LDAP_BASE_DN}" class LDAPAuthenticator: @@ -66,8 +65,8 @@ class LDAPAuthenticator: print(f"❌ Authentication failed for user: {username}") return False - except LDAPBindError as e: - print(f"❌ Authentication failed: Invalid credentials") + except LDAPBindError: + print("❌ Authentication failed: Invalid credentials") return False except LDAPException as e: print(f"❌ LDAP error: {e}") @@ -76,7 +75,7 @@ class LDAPAuthenticator: print(f"❌ Unexpected error: {e}") return False - def get_user_info(self, username: str, admin_dn: str, admin_password: str) -> Optional[Dict]: + def get_user_info(self, username: str, admin_dn: str, admin_password: str) -> dict | None: """ Retrieve detailed information about a user. @@ -121,7 +120,7 @@ class LDAPAuthenticator: print(f"Error retrieving user info: {e}") return None - def get_user_groups(self, username: str, admin_dn: str, admin_password: str) -> List[str]: + def get_user_groups(self, username: str, admin_dn: str, admin_password: str) -> list[str]: """ Get all groups that a user belongs to. @@ -154,7 +153,7 @@ class LDAPAuthenticator: print(f"Error retrieving user groups: {e}") return [] - def list_all_users(self, admin_dn: str, admin_password: str) -> List[Dict]: + def list_all_users(self, admin_dn: str, admin_password: str) -> list[dict]: """ List all users in the directory. @@ -192,7 +191,7 @@ class LDAPAuthenticator: return [] -def print_user_info(user_info: Dict): +def print_user_info(user_info: dict): """Pretty print user information.""" print("\n" + "=" * 50) print("USER INFORMATION") @@ -225,10 +224,10 @@ def main(): args = parser.parse_args() # Admin credentials for retrieving user info - admin_dn = "cn=admin,dc=testing,dc=local" - admin_password = "admin_password" + admin_dn = os.environ.get("LDAP_ADMIN_DN", f"cn=admin,{LDAP_BASE_DN}") + admin_password = os.environ.get("LDAP_ADMIN_PASSWORD", "admin_password") - print(f"\n🔐 LDAP Authentication Example") + print("\nLDAP Authentication Example") print(f"Server: {args.server}\n") # Initialize authenticator diff --git a/pyproject.toml b/pyproject.toml index 46c1853..9283491 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "ldap-docker" version = "0.1.0" -description = "A development tool for running OpenLDAP with SSL in Docker" +description = "Docker-based OpenLDAP server with SSL/TLS support and test users for development" readme = "README.md" requires-python = ">=3.9" license = { text = "MIT" } @@ -12,44 +12,22 @@ authors = [ dependencies = [ "ldap3>=2.9.1", "cryptography>=41.0.0", - "click>=8.1.0", - "python-dotenv>=1.0.0", - "pyyaml>=6.0.0", ] -[project.optional-dependencies] -dev = [ - "pytest>=7.4.0", - "pytest-cov>=4.1.0", - "black>=23.0.0", - "ruff>=0.1.0", - "mypy>=1.5.0", -] - -[project.scripts] -ldap-docker = "scripts.cli:main" - [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["scripts", "tests"] +packages = ["scripts"] [dependency-groups] dev = [ "pytest>=7.4.0", "pytest-cov>=4.1.0", - "black>=23.0.0", "ruff>=0.1.0", - "mypy>=1.5.0", ] -[tool.black] -line-length = 100 -target-version = ["py39", "py310", "py311", "py312"] -include = '\.pyi?$' - [tool.ruff] line-length = 100 target-version = "py39" @@ -63,20 +41,13 @@ select = [ "UP", # pyupgrade ] ignore = [ - "E501", # line too long (handled by black) + "E501", # line too long (handled by ruff format) "B008", # do not perform function calls in argument defaults ] [tool.ruff.per-file-ignores] "__init__.py" = ["F401"] -[tool.mypy] -python_version = "3.9" -warn_return_any = true -warn_unused_configs = true -disallow_untyped_defs = false -ignore_missing_imports = true - [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py"] diff --git a/quickstart.sh b/quickstart.sh deleted file mode 100755 index 76909de..0000000 --- a/quickstart.sh +++ /dev/null @@ -1,381 +0,0 @@ -#!/usr/bin/env bash -# -# Quick Start Script for LDAP Docker -# This script will guide you through setting up and starting the LDAP server -# - -set -e # Exit on error - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Emoji support (works on most terminals) -CHECK_MARK="✅" -CROSS_MARK="❌" -WARNING="⚠️" -ROCKET="🚀" -GEAR="⚙️" - -# Get the directory where this script is located -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -cd "$SCRIPT_DIR" - -# Functions -print_header() { - echo -e "${BLUE}================================================${NC}" - echo -e "${BLUE}$1${NC}" - echo -e "${BLUE}================================================${NC}" - echo "" -} - -print_success() { - echo -e "${GREEN}${CHECK_MARK} $1${NC}" -} - -print_error() { - echo -e "${RED}${CROSS_MARK} $1${NC}" -} - -print_warning() { - echo -e "${YELLOW}${WARNING} $1${NC}" -} - -print_info() { - echo -e "${BLUE}${GEAR} $1${NC}" -} - -# Check if command exists -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -# Check Docker -check_docker() { - print_info "Checking Docker..." - - if command_exists docker; then - if docker version >/dev/null 2>&1; then - print_success "Docker is installed and running" - return 0 - else - print_error "Docker is installed but not running" - echo "" - echo "Please start Docker or Rancher Desktop and try again." - return 1 - fi - else - print_error "Docker is not installed" - echo "" - echo "Please install one of the following:" - echo " - Rancher Desktop (recommended for MacOS): https://rancherdesktop.io/" - echo " - Docker Desktop: https://www.docker.com/products/docker-desktop" - return 1 - fi -} - -# Check docker-compose -check_docker_compose() { - print_info "Checking docker-compose..." - - if command_exists docker-compose; then - print_success "docker-compose is available" - return 0 - elif docker compose version >/dev/null 2>&1; then - print_success "docker compose (v2) is available" - return 0 - else - print_warning "docker-compose not found" - echo "Docker Compose is usually included with Docker Desktop and Rancher Desktop." - echo "If you're using a standalone Docker installation, please install docker-compose." - return 1 - fi -} - -# Check Python -check_python() { - print_info "Checking Python..." - - if command_exists python3; then - PYTHON_VERSION=$(python3 --version | cut -d' ' -f2) - print_success "Python ${PYTHON_VERSION} is installed" - return 0 - elif command_exists python; then - PYTHON_VERSION=$(python --version | cut -d' ' -f2) - print_success "Python ${PYTHON_VERSION} is installed" - return 0 - else - print_error "Python is not installed" - echo "Please install Python 3.9 or higher." - return 1 - fi -} - -# Check/Install UV -check_install_uv() { - print_info "Checking UV package manager..." - - if command_exists uv; then - UV_VERSION=$(uv --version | cut -d' ' -f2 || echo "unknown") - print_success "UV ${UV_VERSION} is installed" - return 0 - else - print_warning "UV is not installed" - echo "" - echo "UV is a fast Python package manager (recommended for this project)." - read -p "Would you like to install UV now? [Y/n] " -n 1 -r - echo - - if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then - print_info "Installing UV..." - curl -LsSf https://astral.sh/uv/install.sh | sh - - # Source the shell config to get UV in PATH - export PATH="$HOME/.cargo/bin:$PATH" - - if command_exists uv; then - print_success "UV installed successfully" - return 0 - else - print_error "UV installation may have completed but it's not in PATH" - echo "Please restart your terminal and run this script again." - return 1 - fi - else - print_warning "Skipping UV installation" - echo "You can install dependencies manually with pip if needed." - return 1 - fi - fi -} - -# Check certificates -check_certificates() { - print_info "Checking SSL certificates..." - - if [[ -f "certs/ca.crt" && -f "certs/server.crt" && -f "certs/server.key" ]]; then - print_success "SSL certificates found" - return 0 - else - print_warning "SSL certificates not found" - return 1 - fi -} - -# Generate certificates -generate_certificates() { - print_info "Generating SSL certificates..." - - if command_exists uv && [[ -f "scripts/generate_certs.py" ]]; then - uv run python scripts/generate_certs.py - print_success "Certificates generated" - elif command_exists python3; then - python3 scripts/generate_certs.py - print_success "Certificates generated" - else - print_error "Cannot generate certificates - Python not available" - return 1 - fi -} - -# Install dependencies -install_dependencies() { - print_info "Installing Python dependencies..." - - if command_exists uv; then - uv sync - print_success "Dependencies installed with UV" - elif command_exists python3 && command_exists pip3; then - pip3 install -e . - print_success "Dependencies installed with pip" - else - print_warning "Cannot install dependencies automatically" - echo "Please install dependencies manually." - return 1 - fi -} - -# Start LDAP server -start_server() { - print_info "Starting LDAP server..." - - if command_exists docker-compose; then - docker-compose up -d - else - docker compose up -d - fi - - print_success "LDAP server started" - - # Wait a bit for the server to initialize - print_info "Waiting for server to initialize (10 seconds)..." - sleep 10 -} - -# Test connection -test_connection() { - print_info "Testing LDAP connection..." - - # Simple check if container is running - if docker ps | grep -q "ldap-server"; then - print_success "LDAP server container is running" - return 0 - else - print_warning "LDAP server container may not be fully started yet" - return 1 - fi -} - -# Print final information -print_final_info() { - echo "" - print_header "${ROCKET} LDAP Server is Ready! ${ROCKET}" - - echo "Services are now available at:" - echo "" - echo " ${GREEN}LDAP:${NC} ldap://localhost:389" - echo " ${GREEN}LDAPS:${NC} ldaps://localhost:636" - echo " ${GREEN}Admin UI:${NC} http://localhost:8080" - echo "" - echo "Admin Credentials:" - echo " ${BLUE}DN:${NC} cn=admin,dc=testing,dc=local" - echo " ${BLUE}Password:${NC} admin_password" - echo "" - echo "Test Users (password: password123):" - echo " - jdoe (John Doe)" - echo " - jsmith (Jane Smith)" - echo " - testuser (Test User)" - echo "" - echo "Useful Commands:" - echo " ${BLUE}make logs${NC} - View server logs" - echo " ${BLUE}make test-users${NC} - List all users" - echo " ${BLUE}make status${NC} - Check server status" - echo " ${BLUE}make stop${NC} - Stop the server" - echo " ${BLUE}make help${NC} - See all available commands" - echo "" - echo "Documentation:" - echo " - README.md for full documentation" - echo " - certs/README.md for certificate management" - echo "" -} - -# Main script -main() { - clear - print_header "${ROCKET} LDAP Docker Quick Start ${ROCKET}" - - echo "This script will set up and start your LDAP development server." - echo "" - - # Step 1: Check prerequisites - print_header "Step 1: Checking Prerequisites" - - PREREQ_OK=true - - check_docker || PREREQ_OK=false - check_docker_compose || PREREQ_OK=false - check_python || PREREQ_OK=false - - if [ "$PREREQ_OK" = false ]; then - echo "" - print_error "Some prerequisites are missing. Please install them and try again." - exit 1 - fi - - echo "" - print_success "All prerequisites are met!" - echo "" - - # Step 2: UV installation (optional but recommended) - print_header "Step 2: Package Manager Setup" - check_install_uv - HAS_UV=$? - echo "" - - # Step 3: SSL Certificates - print_header "Step 3: SSL Certificate Setup" - - if check_certificates; then - echo "" - print_info "Existing certificates will be used." - read -p "Regenerate certificates? [y/N] " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - generate_certificates - fi - else - echo "" - echo "SSL certificates are required for LDAPS (secure LDAP)." - echo "" - echo "You can:" - echo " 1. Generate self-signed certificates now (recommended for quick start)" - echo " 2. Copy certificates from your dev-ca manually to certs/" - echo "" - read -p "Generate self-signed certificates now? [Y/n] " -n 1 -r - echo - - if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then - generate_certificates - else - print_info "Please copy your certificates to certs/ directory:" - echo " - certs/ca.crt" - echo " - certs/server.crt" - echo " - certs/server.key" - echo "" - read -p "Press Enter when ready to continue..." - - if ! check_certificates; then - print_error "Certificates still not found. Exiting." - exit 1 - fi - fi - fi - - echo "" - - # Step 4: Install dependencies (optional) - if [ $HAS_UV -eq 0 ]; then - print_header "Step 4: Installing Dependencies" - install_dependencies - echo "" - else - print_info "Skipping dependency installation (UV not available)" - echo "" - fi - - # Step 5: Start server - print_header "Step 5: Starting LDAP Server" - start_server - echo "" - - # Step 6: Test - print_header "Step 6: Verifying Installation" - test_connection - echo "" - - # Success! - print_final_info - - # Offer to view logs - echo "" - read -p "View server logs now? [y/N] " -n 1 -r - echo - if [[ $REPLY =~ ^[Yy]$ ]]; then - echo "" - print_info "Showing logs (press Ctrl+C to exit)..." - sleep 2 - if command_exists docker-compose; then - docker-compose logs -f openldap - else - docker compose logs -f openldap - fi - fi -} - -# Run main function -main - -exit 0 diff --git a/scripts/__init__.py b/scripts/__init__.py index e0b4177..c09d832 100644 --- a/scripts/__init__.py +++ b/scripts/__init__.py @@ -5,9 +5,7 @@ This package contains management and utility scripts for the LDAP Docker development environment. Modules: - - cli: Command-line interface for managing LDAP server - generate_certs: SSL/TLS certificate generation utility """ __version__ = "0.1.0" -__all__ = ["cli", "generate_certs"] diff --git a/scripts/cli.py b/scripts/cli.py deleted file mode 100644 index a1c64e5..0000000 --- a/scripts/cli.py +++ /dev/null @@ -1,464 +0,0 @@ -#!/usr/bin/env python3 -""" -CLI tool for managing the LDAP Docker development environment. - -This tool provides convenient commands for starting, stopping, and testing -the LDAP server, as well as managing test users and certificates. -""" - -import os -import subprocess -import sys -import time -from pathlib import Path -from typing import Optional - -import click - -try: - import ldap3 - from ldap3 import ALL, Connection, Server - from ldap3.core.exceptions import LDAPException -except ImportError: - ldap3 = None - - -# Constants -PROJECT_ROOT = Path(__file__).parent.parent -CERTS_DIR = PROJECT_ROOT / "certs" -LDIF_DIR = PROJECT_ROOT / "ldif" -DEFAULT_HOST = "localhost" -DEFAULT_PORT = int(os.environ.get("LDAP_PORT", "389")) -DEFAULT_LDAPS_PORT = int(os.environ.get("LDAPS_PORT", "636")) -DEFAULT_BASE_DN = "dc=testing,dc=local" -DEFAULT_ADMIN_DN = "cn=admin,dc=testing,dc=local" -DEFAULT_ADMIN_PASSWORD = "admin_password" - - -def run_command(cmd: list[str], cwd: Optional[Path] = None, check: bool = True) -> subprocess.CompletedProcess: - """Run a shell command and return the result.""" - if cwd is None: - cwd = PROJECT_ROOT - - try: - result = subprocess.run( - cmd, - cwd=cwd, - capture_output=True, - text=True, - check=check, - ) - return result - except subprocess.CalledProcessError as e: - click.echo(f"Error running command: {' '.join(cmd)}", err=True) - click.echo(f"Exit code: {e.returncode}", err=True) - if e.stdout: - click.echo(f"stdout: {e.stdout}", err=True) - if e.stderr: - click.echo(f"stderr: {e.stderr}", err=True) - raise - - -def check_docker(): - """Check if Docker is available.""" - try: - result = run_command(["docker", "version"], check=False) - if result.returncode != 0: - click.echo("Error: Docker is not running or not installed.", err=True) - click.echo("Please ensure Docker (or Rancher Desktop) is running.", err=True) - sys.exit(1) - except FileNotFoundError: - click.echo("Error: Docker command not found.", err=True) - click.echo("Please install Docker or Rancher Desktop.", err=True) - sys.exit(1) - - -def check_certificates(): - """Check if SSL certificates exist.""" - required_files = ["ca.crt", "server.crt", "server.key"] - missing = [f for f in required_files if not (CERTS_DIR / f).exists()] - - if missing: - click.echo("⚠️ Warning: Missing SSL certificate files:", err=True) - for f in missing: - click.echo(f" - {CERTS_DIR / f}", err=True) - click.echo("\nYou can:", err=True) - click.echo(" 1. Copy your dev-ca certificates to the certs/ directory", err=True) - click.echo(" 2. Generate self-signed certificates: ldap-docker certs generate", err=True) - return False - return True - - -@click.group() -@click.version_option(version="0.1.0") -def cli(): - """LDAP Docker Development Tool - Manage your OpenLDAP development environment.""" - pass - - -@cli.group() -def server(): - """Manage the LDAP server container.""" - pass - - -@server.command("start") -@click.option("--detach", "-d", is_flag=True, default=True, help="Run in detached mode") -@click.option("--build", is_flag=True, help="Build images before starting") -def server_start(detach: bool, build: bool): - """Start the LDAP server and optional phpLDAPadmin.""" - check_docker() - check_certificates() - - cmd = ["docker-compose", "up"] - if detach: - cmd.append("-d") - if build: - cmd.append("--build") - - click.echo("Starting LDAP server...") - result = run_command(cmd) - - if result.returncode == 0: - click.echo("✅ LDAP server started successfully!") - click.echo(f"\nLDAP server is available at:") - click.echo(f" - LDAP: ldap://localhost:389") - click.echo(f" - LDAPS: ldaps://localhost:636") - click.echo(f" - Admin: http://localhost:8080 (phpLDAPadmin)") - click.echo(f"\nAdmin credentials:") - click.echo(f" - DN: {DEFAULT_ADMIN_DN}") - click.echo(f" - Password: {DEFAULT_ADMIN_PASSWORD}") - - if detach: - click.echo("\nWaiting for server to be ready...") - time.sleep(5) - # Try to check if server is responding - result = run_command( - ["docker-compose", "ps", "--filter", "status=running"], - check=False - ) - if "ldap-server" in result.stdout: - click.echo("✅ Server is running") - - -@server.command("stop") -def server_stop(): - """Stop the LDAP server.""" - check_docker() - - click.echo("Stopping LDAP server...") - run_command(["docker-compose", "stop"]) - click.echo("✅ LDAP server stopped") - - -@server.command("restart") -def server_restart(): - """Restart the LDAP server.""" - check_docker() - - click.echo("Restarting LDAP server...") - run_command(["docker-compose", "restart"]) - click.echo("✅ LDAP server restarted") - - click.echo("\nWaiting for server to be ready...") - time.sleep(5) - - -@server.command("down") -@click.option("--volumes", "-v", is_flag=True, help="Remove volumes (deletes all data)") -def server_down(volumes: bool): - """Stop and remove the LDAP server containers.""" - check_docker() - - cmd = ["docker-compose", "down"] - if volumes: - if not click.confirm("⚠️ This will delete all LDAP data. Continue?"): - click.echo("Aborted.") - return - cmd.append("-v") - - click.echo("Removing LDAP server containers...") - run_command(cmd) - click.echo("✅ Containers removed") - - -@server.command("logs") -@click.option("--follow", "-f", is_flag=True, help="Follow log output") -@click.option("--tail", "-n", default=100, help="Number of lines to show from the end") -@click.option("--service", default="openldap", help="Service to show logs for") -def server_logs(follow: bool, tail: int, service: str): - """View LDAP server logs.""" - check_docker() - - cmd = ["docker-compose", "logs", f"--tail={tail}"] - if follow: - cmd.append("-f") - cmd.append(service) - - # For follow mode, we want to pass through to the terminal - try: - subprocess.run(cmd, cwd=PROJECT_ROOT) - except KeyboardInterrupt: - click.echo("\n") - - -@server.command("status") -def server_status(): - """Check LDAP server status.""" - check_docker() - - result = run_command(["docker-compose", "ps"], check=False) - click.echo(result.stdout) - - -@cli.group() -def certs(): - """Manage SSL/TLS certificates.""" - pass - - -@certs.command("generate") -@click.option("--force", is_flag=True, help="Overwrite existing certificates") -@click.option("--hostname", default="ldap.testing.local", help="Server hostname") -def certs_generate(force: bool, hostname: str): - """Generate self-signed SSL certificates for development.""" - script_path = PROJECT_ROOT / "scripts" / "generate_certs.py" - - if not script_path.exists(): - click.echo(f"Error: Certificate generation script not found: {script_path}", err=True) - sys.exit(1) - - cmd = [sys.executable, str(script_path), "--hostname", hostname] - if force: - cmd.append("--force") - - try: - subprocess.run(cmd, check=True) - except subprocess.CalledProcessError: - click.echo("Failed to generate certificates", err=True) - sys.exit(1) - - -@certs.command("check") -def certs_check(): - """Verify SSL certificates.""" - required_files = { - "ca.crt": "CA Certificate", - "server.crt": "Server Certificate", - "server.key": "Server Private Key", - } - - click.echo("Checking SSL certificates...\n") - - all_exist = True - for filename, description in required_files.items(): - filepath = CERTS_DIR / filename - if filepath.exists(): - size = filepath.stat().st_size - click.echo(f"✅ {description}: {filepath} ({size} bytes)") - else: - click.echo(f"❌ {description}: {filepath} (missing)") - all_exist = False - - if all_exist: - click.echo("\n✅ All required certificates are present") - - # Try to verify the certificate chain - try: - result = run_command([ - "openssl", "verify", "-CAfile", - str(CERTS_DIR / "ca.crt"), - str(CERTS_DIR / "server.crt") - ], check=False) - - if result.returncode == 0: - click.echo("✅ Certificate chain is valid") - else: - click.echo("⚠️ Certificate chain verification failed") - click.echo(result.stderr) - except FileNotFoundError: - click.echo("ℹ️ OpenSSL not found, skipping certificate verification") - else: - click.echo("\n❌ Some certificates are missing") - click.echo("Run 'ldap-docker certs generate' to create them") - sys.exit(1) - - -@cli.group() -def test(): - """Test LDAP server connectivity and queries.""" - pass - - -@test.command("connection") -@click.option("--host", default=DEFAULT_HOST, help="LDAP server host") -@click.option("--port", default=DEFAULT_PORT, help="LDAP server port") -@click.option("--use-ssl", is_flag=True, help="Use LDAPS instead of LDAP") -def test_connection(host: str, port: int, use_ssl: bool): - """Test basic connection to LDAP server.""" - if ldap3 is None: - click.echo("Error: ldap3 library not installed", err=True) - click.echo("Install it with: uv pip install ldap3", err=True) - sys.exit(1) - - if use_ssl: - port = DEFAULT_LDAPS_PORT - url = f"ldaps://{host}:{port}" - else: - url = f"ldap://{host}:{port}" - - click.echo(f"Testing connection to {url}...") - - try: - server = Server(url, get_info=ALL, use_ssl=use_ssl) - conn = Connection(server, auto_bind=True) - - click.echo("✅ Successfully connected to LDAP server") - click.echo(f"\nServer info:") - click.echo(f" Vendor: {server.info.vendor_name if server.info else 'Unknown'}") - click.echo(f" Version: {server.info.vendor_version if server.info else 'Unknown'}") - - conn.unbind() - - except LDAPException as e: - click.echo(f"❌ Connection failed: {e}", err=True) - sys.exit(1) - - -@test.command("auth") -@click.option("--host", default=DEFAULT_HOST, help="LDAP server host") -@click.option("--port", default=DEFAULT_PORT, help="LDAP server port") -@click.option("--use-ssl", is_flag=True, help="Use LDAPS") -@click.option("--user", default=DEFAULT_ADMIN_DN, help="User DN") -@click.option("--password", default=DEFAULT_ADMIN_PASSWORD, help="Password") -def test_auth(host: str, port: int, use_ssl: bool, user: str, password: str): - """Test authentication with LDAP server.""" - if ldap3 is None: - click.echo("Error: ldap3 library not installed", err=True) - click.echo("Install it with: uv pip install ldap3", err=True) - sys.exit(1) - - if use_ssl: - port = DEFAULT_LDAPS_PORT - url = f"ldaps://{host}:{port}" - else: - url = f"ldap://{host}:{port}" - - click.echo(f"Testing authentication to {url}...") - click.echo(f"User: {user}") - - try: - server = Server(url, get_info=ALL, use_ssl=use_ssl) - conn = Connection(server, user=user, password=password, auto_bind=True) - - click.echo("✅ Authentication successful") - - # Try to perform a simple search - conn.search(DEFAULT_BASE_DN, "(objectClass=*)", search_scope="BASE") - if conn.entries: - click.echo(f"✅ Base DN accessible: {DEFAULT_BASE_DN}") - - conn.unbind() - - except LDAPException as e: - click.echo(f"❌ Authentication failed: {e}", err=True) - sys.exit(1) - - -@test.command("users") -@click.option("--host", default=DEFAULT_HOST, help="LDAP server host") -@click.option("--port", default=DEFAULT_PORT, help="LDAP server port") -@click.option("--use-ssl", is_flag=True, help="Use LDAPS") -def test_users(host: str, port: int, use_ssl: bool): - """List all users in the LDAP directory.""" - if ldap3 is None: - click.echo("Error: ldap3 library not installed", err=True) - click.echo("Install it with: uv pip install ldap3", err=True) - sys.exit(1) - - if use_ssl: - port = DEFAULT_LDAPS_PORT - url = f"ldaps://{host}:{port}" - else: - url = f"ldap://{host}:{port}" - - try: - server = Server(url, get_info=ALL, use_ssl=use_ssl) - conn = Connection( - server, - user=DEFAULT_ADMIN_DN, - password=DEFAULT_ADMIN_PASSWORD, - auto_bind=True - ) - - # Search for all users - conn.search( - DEFAULT_BASE_DN, - "(objectClass=inetOrgPerson)", - attributes=["uid", "cn", "mail", "uidNumber"] - ) - - if conn.entries: - click.echo(f"Found {len(conn.entries)} user(s):\n") - for entry in conn.entries: - click.echo(f" - {entry.cn}: {entry.uid} ({entry.mail})") - else: - click.echo("No users found") - - conn.unbind() - - except LDAPException as e: - click.echo(f"❌ Query failed: {e}", err=True) - sys.exit(1) - - -@cli.command("init") -def init(): - """Initialize the LDAP Docker environment.""" - click.echo("Initializing LDAP Docker environment...\n") - - # Check Docker - click.echo("1. Checking Docker...") - check_docker() - click.echo(" ✅ Docker is available\n") - - # Check certificates - click.echo("2. Checking SSL certificates...") - if not check_certificates(): - if click.confirm("\nGenerate self-signed certificates now?", default=True): - certs_generate.callback(force=False, hostname="ldap.testing.local") - else: - click.echo("\nℹ️ You can generate certificates later with: ldap-docker certs generate") - click.echo(" Or copy your dev-ca certificates to the certs/ directory") - else: - click.echo(" ✅ Certificates are present\n") - - # Start server - click.echo("\n3. Starting LDAP server...") - if click.confirm("Start the LDAP server now?", default=True): - server_start.callback(detach=True, build=False) - else: - click.echo("\nℹ️ You can start the server later with: ldap-docker server start") - - click.echo("\n✅ Initialization complete!") - click.echo("\nUseful commands:") - click.echo(" - View logs: ldap-docker server logs -f") - click.echo(" - Test connection: ldap-docker test connection") - click.echo(" - List users: ldap-docker test users") - click.echo(" - Stop server: ldap-docker server stop") - - -def main(): - """Entry point for the CLI.""" - try: - cli() - except KeyboardInterrupt: - click.echo("\n\nInterrupted by user") - sys.exit(130) - except Exception as e: - click.echo(f"Error: {e}", err=True) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/scripts/generate_certs.py b/scripts/generate_certs.py index ca94e96..86c6718 100644 --- a/scripts/generate_certs.py +++ b/scripts/generate_certs.py @@ -9,7 +9,7 @@ For production, use proper certificates from your dev-ca or a trusted CA. import argparse import ipaddress import sys -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path try: @@ -17,7 +17,7 @@ try: from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa - from cryptography.x509.oid import ExtensionOID, NameOID + from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID except ImportError: print("Error: cryptography library not found.") print("Install it with: uv pip install cryptography") @@ -55,8 +55,8 @@ def generate_ca_certificate( .issuer_name(issuer) .public_key(private_key.public_key()) .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=days_valid)) + .not_valid_before(datetime.now(timezone.utc)) + .not_valid_after(datetime.now(timezone.utc) + timedelta(days=days_valid)) .add_extension( x509.BasicConstraints(ca=True, path_length=None), critical=True, @@ -90,7 +90,7 @@ def generate_server_certificate( ca_cert: x509.Certificate, ca_key: rsa.RSAPrivateKey, hostname: str = "ldap.testing.local", - san_list: list[str] = None, + san_list: list[str] | None = None, days_valid: int = 365, ) -> x509.Certificate: """Generate a server certificate signed by the CA.""" @@ -123,8 +123,8 @@ def generate_server_certificate( .issuer_name(ca_cert.subject) .public_key(private_key.public_key()) .serial_number(x509.random_serial_number()) - .not_valid_before(datetime.utcnow()) - .not_valid_after(datetime.utcnow() + timedelta(days=days_valid)) + .not_valid_before(datetime.now(timezone.utc)) + .not_valid_after(datetime.now(timezone.utc) + timedelta(days=days_valid)) .add_extension( x509.SubjectAlternativeName(san_entries), critical=False, @@ -148,7 +148,7 @@ def generate_server_certificate( critical=True, ) .add_extension( - x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]), + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False, ) .add_extension( @@ -236,9 +236,7 @@ def main(): # Check if certificates already exist if not args.force: - existing = [ - p for p in [ca_cert_path, server_cert_path, server_key_path] if p.exists() - ] + existing = [p for p in [ca_cert_path, server_cert_path, server_key_path] if p.exists()] if existing: print("Error: The following certificate files already exist:") for p in existing: diff --git a/scripts/ldapsearch_helper.sh b/scripts/ldapsearch_helper.sh deleted file mode 100755 index 6d1ad28..0000000 --- a/scripts/ldapsearch_helper.sh +++ /dev/null @@ -1,195 +0,0 @@ -#!/usr/bin/env bash -# -# LDAP Search Helper -# Generates ldapsearch commands configured for your environment -# - -set -e - -# Colors -GREEN='\033[0;32m' -BLUE='\033[0;34m' -YELLOW='\033[1;33m' -NC='\033[0m' - -# Get the directory where this script is located -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" - -# Load environment variables from .env if it exists -if [ -f "$PROJECT_ROOT/.env" ]; then - source "$PROJECT_ROOT/.env" -fi - -# Set defaults if not in environment -LDAP_PORT=${LDAP_PORT:-389} -LDAPS_PORT=${LDAPS_PORT:-636} -LDAP_BASE_DN=${LDAP_BASE_DN:-dc=testing,dc=local} -LDAP_ADMIN_PASSWORD=${LDAP_ADMIN_PASSWORD:-admin_password} - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}LDAP Search Command Generator${NC}" -echo -e "${BLUE}========================================${NC}" -echo "" -echo "Configuration:" -echo " LDAP Port: $LDAP_PORT" -echo " LDAPS Port: $LDAPS_PORT" -echo " Base DN: $LDAP_BASE_DN" -echo "" - -# Function to print a command example -print_cmd() { - local description="$1" - local command="$2" - - echo -e "${GREEN}# $description${NC}" - echo -e "${YELLOW}$command${NC}" - echo "" -} - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}Basic Connection Tests${NC}" -echo -e "${BLUE}========================================${NC}" -echo "" - -print_cmd "Test server is responding (anonymous bind)" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -x -b \"\" -s base" - -print_cmd "Test with admin credentials" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"$LDAP_BASE_DN\"" - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}Search Users${NC}" -echo -e "${BLUE}========================================${NC}" -echo "" - -print_cmd "List all users" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=people,$LDAP_BASE_DN\" \"(objectClass=inetOrgPerson)\" uid cn mail" - -print_cmd "Search for specific user (jdoe)" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"$LDAP_BASE_DN\" \"(uid=jdoe)\"" - -print_cmd "Test user authentication (as jdoe)" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"uid=jdoe,ou=people,$LDAP_BASE_DN\" -w password123 -b \"$LDAP_BASE_DN\" \"(uid=jdoe)\"" - -print_cmd "Get all user attributes for jdoe" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=people,$LDAP_BASE_DN\" \"(uid=jdoe)\" '*' '+'" - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}Search Groups${NC}" -echo -e "${BLUE}========================================${NC}" -echo "" - -print_cmd "List all groups" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=groups,$LDAP_BASE_DN\" \"(objectClass=groupOfNames)\" cn member" - -print_cmd "Find groups for user jdoe" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=groups,$LDAP_BASE_DN\" \"(member=uid=jdoe,ou=people,$LDAP_BASE_DN)\" cn" - -print_cmd "Get members of developers group" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=groups,$LDAP_BASE_DN\" \"(cn=developers)\" member" - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}LDAPS (SSL/TLS) Commands${NC}" -echo -e "${BLUE}========================================${NC}" -echo "" - -print_cmd "Test LDAPS connection" \ -"ldapsearch -H ldaps://localhost:$LDAPS_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"$LDAP_BASE_DN\"" - -print_cmd "LDAPS with CA certificate verification" \ -"LDAPTLS_CACERT=$PROJECT_ROOT/certs/ca.crt ldapsearch -H ldaps://localhost:$LDAPS_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"$LDAP_BASE_DN\"" - -print_cmd "Check SSL certificate" \ -"openssl s_client -connect localhost:$LDAPS_PORT -CAfile $PROJECT_ROOT/certs/ca.crt -showcerts" - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}Secure Commands (Password Prompt)${NC}" -echo -e "${BLUE}========================================${NC}" -echo "" - -print_cmd "Admin search with password prompt (more secure)" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -W -b \"$LDAP_BASE_DN\"" - -print_cmd "User authentication with password prompt" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"uid=jdoe,ou=people,$LDAP_BASE_DN\" -W -b \"$LDAP_BASE_DN\" \"(uid=jdoe)\"" - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}Advanced Queries${NC}" -echo -e "${BLUE}========================================${NC}" -echo "" - -print_cmd "Search with wildcard (all users starting with 'j')" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=people,$LDAP_BASE_DN\" \"(uid=j*)\" uid cn" - -print_cmd "Search by email domain" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=people,$LDAP_BASE_DN\" \"(mail=*@testing.local)\" uid mail" - -print_cmd "Count total users" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"ou=people,$LDAP_BASE_DN\" \"(objectClass=inetOrgPerson)\" dn | grep -c '^dn:'" - -print_cmd "Export entire directory to LDIF file" \ -"ldapsearch -H ldap://localhost:$LDAP_PORT -D \"cn=admin,$LDAP_BASE_DN\" -w $LDAP_ADMIN_PASSWORD -b \"$LDAP_BASE_DN\" -LLL > ldap_backup.ldif" - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}Quick Reference${NC}" -echo -e "${BLUE}========================================${NC}" -echo "" -echo "Common flags:" -echo " -H : LDAP URI (ldap:// or ldaps://)" -echo " -D : Bind DN (user to authenticate as)" -echo " -w : Password (visible in process list)" -echo " -W : Prompt for password (more secure)" -echo " -x : Use simple authentication" -echo " -b : Base DN to search from" -echo " -s : Scope (base, one, sub)" -echo " -LLL: LDIF output without comments" -echo "" -echo "Test credentials:" -echo " Admin: cn=admin,$LDAP_BASE_DN / $LDAP_ADMIN_PASSWORD" -echo " Test user: uid=jdoe,ou=people,$LDAP_BASE_DN / password123" -echo "" -echo -e "${GREEN}Tip:${NC} Copy any command above and run it in your terminal!" -echo "" - -# Option to run a command interactively -if [ "$1" = "--interactive" ] || [ "$1" = "-i" ]; then - echo "" - echo "Select a command to run:" - echo " 1) List all users" - echo " 2) Search for user jdoe" - echo " 3) Test user authentication" - echo " 4) List all groups" - echo " 5) Test LDAPS connection" - echo " 6) Custom command" - echo "" - read -p "Choice (1-6): " choice - - case $choice in - 1) - ldapsearch -H ldap://localhost:$LDAP_PORT -D "cn=admin,$LDAP_BASE_DN" -w $LDAP_ADMIN_PASSWORD -b "ou=people,$LDAP_BASE_DN" "(objectClass=inetOrgPerson)" uid cn mail - ;; - 2) - ldapsearch -H ldap://localhost:$LDAP_PORT -D "cn=admin,$LDAP_BASE_DN" -w $LDAP_ADMIN_PASSWORD -b "$LDAP_BASE_DN" "(uid=jdoe)" - ;; - 3) - read -p "Enter password for jdoe: " -s user_pass - echo "" - ldapsearch -H ldap://localhost:$LDAP_PORT -D "uid=jdoe,ou=people,$LDAP_BASE_DN" -w "$user_pass" -b "$LDAP_BASE_DN" "(uid=jdoe)" - ;; - 4) - ldapsearch -H ldap://localhost:$LDAP_PORT -D "cn=admin,$LDAP_BASE_DN" -w $LDAP_ADMIN_PASSWORD -b "ou=groups,$LDAP_BASE_DN" "(objectClass=groupOfNames)" cn member - ;; - 5) - LDAPTLS_CACERT=$PROJECT_ROOT/certs/ca.crt ldapsearch -H ldaps://localhost:$LDAPS_PORT -D "cn=admin,$LDAP_BASE_DN" -w $LDAP_ADMIN_PASSWORD -b "$LDAP_BASE_DN" - ;; - 6) - read -p "Enter custom filter: " filter - ldapsearch -H ldap://localhost:$LDAP_PORT -D "cn=admin,$LDAP_BASE_DN" -w $LDAP_ADMIN_PASSWORD -b "$LDAP_BASE_DN" "$filter" - ;; - *) - echo "Invalid choice" - exit 1 - ;; - esac -fi diff --git a/tests/test_generate_certs.py b/tests/test_generate_certs.py index 9651ae6..ee6e05e 100644 --- a/tests/test_generate_certs.py +++ b/tests/test_generate_certs.py @@ -5,8 +5,9 @@ This module contains unit tests for the generate_certs.py script. """ import tempfile -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path +from typing import TYPE_CHECKING, cast import pytest @@ -14,18 +15,25 @@ try: from cryptography import x509 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import rsa - from cryptography.x509.oid import NameOID + from cryptography.x509 import BasicConstraints, SubjectAlternativeName + from cryptography.x509.oid import ExtensionOID, NameOID CRYPTO_AVAILABLE = True except ImportError: CRYPTO_AVAILABLE = False + if TYPE_CHECKING: + from cryptography import x509 + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives.asymmetric import rsa + from cryptography.x509 import BasicConstraints, SubjectAlternativeName + from cryptography.x509.oid import ExtensionOID, NameOID # Import the module to test import sys sys.path.insert(0, str(Path(__file__).parent.parent)) -if CRYPTO_AVAILABLE: +if CRYPTO_AVAILABLE or TYPE_CHECKING: from scripts.generate_certs import ( generate_ca_certificate, generate_private_key, @@ -63,10 +71,8 @@ class TestCertificateGeneration: assert cert.subject == cert.issuer # Check basic constraints - basic_constraints = cert.extensions.get_extension_for_oid( - x509.ExtensionOID.BASIC_CONSTRAINTS - ) - assert basic_constraints.value.ca is True + basic_constraints = cert.extensions.get_extension_for_oid(ExtensionOID.BASIC_CONSTRAINTS) + assert cast(BasicConstraints, basic_constraints.value).ca is True def test_generate_ca_certificate_validity(self): """Test that CA certificate has correct validity period.""" @@ -74,11 +80,11 @@ class TestCertificateGeneration: days_valid = 365 cert = generate_ca_certificate(key, days_valid=days_valid) - now = datetime.utcnow() + now = datetime.now(timezone.utc) expected_expiry = now + timedelta(days=days_valid) # Allow 1 minute tolerance for test execution time - assert abs((cert.not_valid_after - expected_expiry).total_seconds()) < 60 + assert abs((cert.not_valid_after_utc - expected_expiry).total_seconds()) < 60 def test_generate_server_certificate(self): """Test that a server certificate can be generated.""" @@ -105,9 +111,9 @@ class TestCertificateGeneration: # Check basic constraints - should not be a CA basic_constraints = server_cert.extensions.get_extension_for_oid( - x509.ExtensionOID.BASIC_CONSTRAINTS + ExtensionOID.BASIC_CONSTRAINTS ) - assert basic_constraints.value.ca is False + assert cast(BasicConstraints, basic_constraints.value).ca is False def test_generate_server_certificate_san(self): """Test that server certificate includes Subject Alternative Names.""" @@ -126,11 +132,15 @@ class TestCertificateGeneration: # Get SAN extension san_ext = server_cert.extensions.get_extension_for_oid( - x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME + ExtensionOID.SUBJECT_ALTERNATIVE_NAME ) # Extract DNS names - dns_names = [name.value for name in san_ext.value if isinstance(name, x509.DNSName)] + dns_names = [ + name.value + for name in cast(SubjectAlternativeName, san_ext.value) + if isinstance(name, x509.DNSName) + ] # Check that all DNS names are present for name in san_list: @@ -191,15 +201,15 @@ class TestCertificateGeneration: # Verify server cert is not a CA server_basic_constraints = server_cert.extensions.get_extension_for_oid( - x509.ExtensionOID.BASIC_CONSTRAINTS + ExtensionOID.BASIC_CONSTRAINTS ) - assert server_basic_constraints.value.ca is False + assert cast(BasicConstraints, server_basic_constraints.value).ca is False # Verify CA cert is a CA ca_basic_constraints = ca_cert.extensions.get_extension_for_oid( - x509.ExtensionOID.BASIC_CONSTRAINTS + ExtensionOID.BASIC_CONSTRAINTS ) - assert ca_basic_constraints.value.ca is True + assert cast(BasicConstraints, ca_basic_constraints.value).ca is True @pytest.mark.skipif(not CRYPTO_AVAILABLE, reason="cryptography library not available") @@ -219,12 +229,12 @@ class TestCertificateValidation: # Check server certificate extensions ext_oids = [ext.oid for ext in server_cert.extensions] - assert x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME in ext_oids - assert x509.ExtensionOID.BASIC_CONSTRAINTS in ext_oids - assert x509.ExtensionOID.KEY_USAGE in ext_oids - assert x509.ExtensionOID.EXTENDED_KEY_USAGE in ext_oids - assert x509.ExtensionOID.SUBJECT_KEY_IDENTIFIER in ext_oids - assert x509.ExtensionOID.AUTHORITY_KEY_IDENTIFIER in ext_oids + assert ExtensionOID.SUBJECT_ALTERNATIVE_NAME in ext_oids + assert ExtensionOID.BASIC_CONSTRAINTS in ext_oids + assert ExtensionOID.KEY_USAGE in ext_oids + assert ExtensionOID.EXTENDED_KEY_USAGE in ext_oids + assert ExtensionOID.SUBJECT_KEY_IDENTIFIER in ext_oids + assert ExtensionOID.AUTHORITY_KEY_IDENTIFIER in ext_oids def test_certificate_validity_dates(self): """Test that certificates have correct validity dates.""" @@ -232,14 +242,14 @@ class TestCertificateValidation: days_valid = 100 cert = generate_ca_certificate(key, days_valid=days_valid) - now = datetime.utcnow() + now = datetime.now(timezone.utc) # Check not_valid_before is around now - assert abs((cert.not_valid_before - now).total_seconds()) < 60 + assert abs((cert.not_valid_before_utc - now).total_seconds()) < 60 # Check not_valid_after is around now + days_valid expected_expiry = now + timedelta(days=days_valid) - assert abs((cert.not_valid_after - expected_expiry).total_seconds()) < 60 + assert abs((cert.not_valid_after_utc - expected_expiry).total_seconds()) < 60 def test_imports():