First commit from the robot
This commit is contained in:
102
.env.example
Normal file
102
.env.example
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# LDAP Docker Environment Configuration
|
||||||
|
# Copy this file to .env and customize as needed
|
||||||
|
# Note: .env is git-ignored to prevent committing secrets
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# LDAP Domain Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# The LDAP domain (e.g., testing.local -> dc=testing,dc=local)
|
||||||
|
LDAP_DOMAIN=testing.local
|
||||||
|
|
||||||
|
# Organization name
|
||||||
|
LDAP_ORGANISATION=Testing Organization
|
||||||
|
|
||||||
|
# Base DN (automatically derived from LDAP_DOMAIN if not set)
|
||||||
|
LDAP_BASE_DN=dc=testing,dc=local
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Admin Credentials
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# LDAP admin password
|
||||||
|
# WARNING: Change this for any environment accessible by others
|
||||||
|
LDAP_ADMIN_PASSWORD=admin_password
|
||||||
|
|
||||||
|
# LDAP config password (for cn=config)
|
||||||
|
LDAP_CONFIG_PASSWORD=config_password
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# SSL/TLS Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Enable TLS/SSL
|
||||||
|
LDAP_TLS=true
|
||||||
|
|
||||||
|
# Certificate filenames (relative to certs/ directory)
|
||||||
|
LDAP_TLS_CRT_FILENAME=server.crt
|
||||||
|
LDAP_TLS_KEY_FILENAME=server.key
|
||||||
|
LDAP_TLS_CA_CRT_FILENAME=ca.crt
|
||||||
|
|
||||||
|
# TLS verification level: never, allow, try, demand
|
||||||
|
LDAP_TLS_VERIFY_CLIENT=try
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Port Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Standard LDAP port (unencrypted)
|
||||||
|
LDAP_PORT=389
|
||||||
|
|
||||||
|
# LDAPS port (SSL/TLS)
|
||||||
|
LDAPS_PORT=636
|
||||||
|
|
||||||
|
# phpLDAPadmin web interface port
|
||||||
|
PHPLDAPADMIN_PORT=8080
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Logging Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# LDAP log level
|
||||||
|
# 0 = no logging, 256 = stats logging, -1 = any logging
|
||||||
|
LDAP_LOG_LEVEL=256
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Container Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Hostname for the LDAP server
|
||||||
|
LDAP_HOSTNAME=ldap.testing.local
|
||||||
|
|
||||||
|
# Container name
|
||||||
|
LDAP_CONTAINER_NAME=ldap-server
|
||||||
|
|
||||||
|
# phpLDAPadmin container name
|
||||||
|
PHPLDAPADMIN_CONTAINER_NAME=ldap-admin
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Optional: Replication Configuration (Advanced)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Enable replication (leave commented for single-server setup)
|
||||||
|
# LDAP_REPLICATION=true
|
||||||
|
# LDAP_REPLICATION_CONFIG_SYNCPROV=binddn="cn=admin,cn=config" bindmethod=simple credentials=$LDAP_CONFIG_PASSWORD searchbase="cn=config" type=refreshAndPersist retry="60 +" timeout=1 starttls=critical
|
||||||
|
# LDAP_REPLICATION_DB_SYNCPROV=binddn="cn=admin,$LDAP_BASE_DN" bindmethod=simple credentials=$LDAP_ADMIN_PASSWORD searchbase="$LDAP_BASE_DN" type=refreshAndPersist interval=00:00:00:10 retry="60 +" timeout=1 starttls=critical
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Optional: Backup Configuration
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Backup directory (uncomment to enable)
|
||||||
|
# BACKUP_DIR=./backups
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Development Settings
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Set to "true" to enable debug output
|
||||||
|
DEBUG=false
|
||||||
|
|
||||||
|
# Timezone (optional)
|
||||||
|
TZ=UTC
|
||||||
117
.gitignore
vendored
Normal file
117
.gitignore
vendored
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# UV specific
|
||||||
|
.uv/
|
||||||
|
uv.lock
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
.hypothesis/
|
||||||
|
|
||||||
|
# Type checking
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Certificates - DO NOT COMMIT PRIVATE KEYS
|
||||||
|
certs/*.key
|
||||||
|
certs/*.crt
|
||||||
|
certs/*.pem
|
||||||
|
certs/*.csr
|
||||||
|
certs/ca.key
|
||||||
|
certs/ca.crt
|
||||||
|
certs/server.key
|
||||||
|
certs/server.crt
|
||||||
|
# Keep the README
|
||||||
|
!certs/README.md
|
||||||
|
|
||||||
|
# Docker volumes and data
|
||||||
|
docker-compose.override.yml
|
||||||
|
.docker/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Backup files
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
|
||||||
|
# Environment files with secrets
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# MacOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
.Thumbs.db
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
288
GETTING_STARTED.md
Normal file
288
GETTING_STARTED.md
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
# Getting Started with LDAP Docker
|
||||||
|
|
||||||
|
Welcome! This guide will help you get your LDAP development server up and running in just a few minutes.
|
||||||
|
|
||||||
|
## What is This?
|
||||||
|
|
||||||
|
LDAP Docker is a ready-to-use OpenLDAP server for development and testing. It comes pre-configured with:
|
||||||
|
|
||||||
|
- ✅ SSL/TLS support (LDAPS)
|
||||||
|
- ✅ Test users and groups
|
||||||
|
- ✅ Web-based admin interface
|
||||||
|
- ✅ Easy certificate management
|
||||||
|
- ✅ Simple Python CLI tools
|
||||||
|
|
||||||
|
Perfect for testing applications that need LDAP authentication without setting up a complex infrastructure.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
You'll need these installed on your Mac:
|
||||||
|
|
||||||
|
1. **Docker or Rancher Desktop** (for running containers)
|
||||||
|
- Rancher Desktop: https://rancherdesktop.io/ (recommended for Mac)
|
||||||
|
- Or Docker Desktop: https://www.docker.com/products/docker-desktop
|
||||||
|
|
||||||
|
2. **Python 3.9+** (usually pre-installed on Mac)
|
||||||
|
```bash
|
||||||
|
python3 --version
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **UV Package Manager** (optional but recommended)
|
||||||
|
```bash
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start (5 Minutes)
|
||||||
|
|
||||||
|
### Option 1: Automated Setup (Easiest)
|
||||||
|
|
||||||
|
Run the quick start script that will guide you through everything:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./quickstart.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This interactive script will:
|
||||||
|
1. Check all prerequisites
|
||||||
|
2. Install UV (if you want)
|
||||||
|
3. Generate SSL certificates
|
||||||
|
4. Start the LDAP server
|
||||||
|
5. Verify everything works
|
||||||
|
|
||||||
|
### Option 2: Manual Setup (Step by Step)
|
||||||
|
|
||||||
|
If you prefer to understand each step:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install Python dependencies
|
||||||
|
make install
|
||||||
|
|
||||||
|
# 2. Generate SSL certificates
|
||||||
|
make certs-generate
|
||||||
|
|
||||||
|
# 3. Start the LDAP server
|
||||||
|
make start
|
||||||
|
|
||||||
|
# 4. Test the connection
|
||||||
|
make test-connection
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it! Your LDAP server is now running.
|
||||||
|
|
||||||
|
## Using Your Own Dev-CA Certificates
|
||||||
|
|
||||||
|
Since you mentioned you maintain a local dev-ca, here's how to use your own certificates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instead of 'make certs-generate', copy your certificates:
|
||||||
|
cp /path/to/your/dev-ca/ca-cert.pem certs/ca.crt
|
||||||
|
cp /path/to/your/dev-ca/ldap-server.crt certs/server.crt
|
||||||
|
cp /path/to/your/dev-ca/ldap-server.key certs/server.key
|
||||||
|
|
||||||
|
# Set proper permissions
|
||||||
|
chmod 644 certs/ca.crt certs/server.crt
|
||||||
|
chmod 600 certs/server.key
|
||||||
|
|
||||||
|
# Then start the server
|
||||||
|
make start
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** Your server certificate should be issued for hostname `ldap.testing.local` or include it as a Subject Alternative Name (SAN).
|
||||||
|
|
||||||
|
## Accessing Your LDAP Server
|
||||||
|
|
||||||
|
Once running, you can access:
|
||||||
|
|
||||||
|
| Service | URL | Purpose |
|
||||||
|
|---------|-----|---------|
|
||||||
|
| **LDAP** | `ldap://localhost:389` | Standard LDAP (unencrypted) |
|
||||||
|
| **LDAPS** | `ldaps://localhost:636` | Secure LDAP with SSL/TLS |
|
||||||
|
| **Admin UI** | `http://localhost:8080` | Web interface (phpLDAPadmin) |
|
||||||
|
|
||||||
|
### Login Credentials
|
||||||
|
|
||||||
|
- **Admin DN:** `cn=admin,dc=testing,dc=local`
|
||||||
|
- **Password:** `admin_password`
|
||||||
|
|
||||||
|
### Test Users (All use password: `password123`)
|
||||||
|
|
||||||
|
- `jdoe` - John Doe (jdoe@testing.local)
|
||||||
|
- `jsmith` - Jane Smith (jsmith@testing.local)
|
||||||
|
- `testuser` - Test User (testuser@testing.local)
|
||||||
|
- `admin` - Admin User (admin@testing.local)
|
||||||
|
|
||||||
|
## Verify Everything Works
|
||||||
|
|
||||||
|
### Test 1: List all users
|
||||||
|
```bash
|
||||||
|
make test-users
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see output like:
|
||||||
|
```
|
||||||
|
Found 4 user(s):
|
||||||
|
|
||||||
|
- Admin User: admin (admin@testing.local)
|
||||||
|
- John Doe: jdoe (jdoe@testing.local)
|
||||||
|
- Jane Smith: jsmith (jsmith@testing.local)
|
||||||
|
- Test User: testuser (testuser@testing.local)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2: Try the web interface
|
||||||
|
|
||||||
|
Open http://localhost:8080 in your browser and login with:
|
||||||
|
- Login DN: `cn=admin,dc=testing,dc=local`
|
||||||
|
- Password: `admin_password`
|
||||||
|
|
||||||
|
### Test 3: Run the example script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python examples/simple_auth.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This authenticates user `jdoe` and displays their information.
|
||||||
|
|
||||||
|
## Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Server management
|
||||||
|
make start # Start LDAP server
|
||||||
|
make stop # Stop LDAP server
|
||||||
|
make restart # Restart LDAP server
|
||||||
|
make logs # View logs (live)
|
||||||
|
make status # Check if running
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
make test-users # List all users
|
||||||
|
make test-auth # Test authentication
|
||||||
|
make test-all # Run all tests
|
||||||
|
|
||||||
|
# Maintenance
|
||||||
|
make clean # Clean build artifacts
|
||||||
|
make down # Stop and remove containers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Now that your LDAP server is running, you can:
|
||||||
|
|
||||||
|
1. **Integrate with Your Application**
|
||||||
|
- See `examples/README.md` for code samples
|
||||||
|
- Point your app to `ldap://localhost:389`
|
||||||
|
|
||||||
|
2. **Add Custom Users**
|
||||||
|
- Edit `ldif/01-users.ldif`
|
||||||
|
- Run `make down-volumes && make start` to reload
|
||||||
|
|
||||||
|
3. **Configure for Your Use Case**
|
||||||
|
- Edit `docker-compose.yml` for custom settings
|
||||||
|
- See `.env.example` for environment variables
|
||||||
|
|
||||||
|
4. **Learn More**
|
||||||
|
- `README.md` - Full documentation
|
||||||
|
- `QUICKREF.md` - Command reference
|
||||||
|
- `certs/README.md` - Certificate management
|
||||||
|
- `examples/README.md` - Integration examples
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Docker is not running"
|
||||||
|
Start Rancher Desktop from your Applications folder. Look for its icon in the menu bar.
|
||||||
|
|
||||||
|
### "Connection refused"
|
||||||
|
Wait 10-30 seconds after starting - the server needs time to initialize:
|
||||||
|
```bash
|
||||||
|
make logs # Watch until you see "slapd starting"
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Certificate errors"
|
||||||
|
Verify certificates exist:
|
||||||
|
```bash
|
||||||
|
ls -la certs/
|
||||||
|
```
|
||||||
|
|
||||||
|
Regenerate if needed:
|
||||||
|
```bash
|
||||||
|
make certs-generate --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### "Port already in use"
|
||||||
|
Check if something is using LDAP ports:
|
||||||
|
```bash
|
||||||
|
lsof -i :389
|
||||||
|
lsof -i :636
|
||||||
|
```
|
||||||
|
|
||||||
|
### Still stuck?
|
||||||
|
Check the full troubleshooting section in `README.md` or view logs:
|
||||||
|
```bash
|
||||||
|
make logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Basics (If You're New)
|
||||||
|
|
||||||
|
Since you mentioned being new to Docker, here are the basics:
|
||||||
|
|
||||||
|
- **Container**: A lightweight, isolated environment running your LDAP server
|
||||||
|
- **Image**: The blueprint for creating containers (we use `osixia/openldap`)
|
||||||
|
- **Volume**: Persistent storage for LDAP data (survives restarts)
|
||||||
|
- **docker-compose**: Tool for managing multi-container applications (LDAP + Admin UI)
|
||||||
|
|
||||||
|
When you run `make start`, Docker:
|
||||||
|
1. Downloads the LDAP image (first time only)
|
||||||
|
2. Creates containers from the image
|
||||||
|
3. Mounts your certificates and data files
|
||||||
|
4. Starts the LDAP service
|
||||||
|
|
||||||
|
The `Makefile` is just a collection of shortcuts for common Docker commands.
|
||||||
|
|
||||||
|
## Building Elsewhere
|
||||||
|
|
||||||
|
This project works on:
|
||||||
|
- ✅ **MacOS** (with Rancher Desktop or Docker Desktop)
|
||||||
|
- ✅ **Linux** (with Docker installed)
|
||||||
|
- ✅ **Windows** (with Docker Desktop + WSL2)
|
||||||
|
|
||||||
|
The same `docker-compose.yml` works everywhere - that's the beauty of Docker!
|
||||||
|
|
||||||
|
To share your setup with a colleague:
|
||||||
|
```bash
|
||||||
|
# Just copy the whole project
|
||||||
|
git clone <your-repo>
|
||||||
|
cd ldap_docker
|
||||||
|
make dev-setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference Card
|
||||||
|
|
||||||
|
Keep these handy:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start server
|
||||||
|
make start
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
make logs
|
||||||
|
|
||||||
|
# List users
|
||||||
|
make test-users
|
||||||
|
|
||||||
|
# Stop server
|
||||||
|
make stop
|
||||||
|
|
||||||
|
# Help
|
||||||
|
make help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support & Documentation
|
||||||
|
|
||||||
|
- **Quick commands**: `make help`
|
||||||
|
- **Full guide**: `README.md`
|
||||||
|
- **Examples**: `examples/README.md`
|
||||||
|
- **Command reference**: `QUICKREF.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Ready to start?** Run `./quickstart.sh` or `make dev-setup` and you'll be up in 5 minutes! 🚀
|
||||||
|
|
||||||
|
If you run into any issues, check the logs with `make logs` or see the Troubleshooting section in `README.md`.
|
||||||
187
Makefile
Normal file
187
Makefile
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
.PHONY: help install init start stop restart down logs status certs-generate certs-check test-connection test-auth test-users clean clean-all
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
|
help: ## Show this help message
|
||||||
|
@echo "LDAP Docker Development Tool"
|
||||||
|
@echo ""
|
||||||
|
@echo "Usage: make [target]"
|
||||||
|
@echo ""
|
||||||
|
@echo "Available targets:"
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
|
install: ## Install Python dependencies with UV
|
||||||
|
@echo "Installing dependencies with UV..."
|
||||||
|
@command -v uv >/dev/null 2>&1 || { echo "Error: uv not found. Install it with: curl -LsSf https://astral.sh/uv/install.sh | sh"; exit 1; }
|
||||||
|
uv sync
|
||||||
|
@echo "✅ Dependencies installed"
|
||||||
|
|
||||||
|
install-dev: ## Install development dependencies
|
||||||
|
@echo "Installing development dependencies with UV..."
|
||||||
|
uv sync --all-extras
|
||||||
|
@echo "✅ Development dependencies installed"
|
||||||
|
|
||||||
|
init: install certs-check ## Initialize the environment (install deps, check certs)
|
||||||
|
@echo ""
|
||||||
|
@echo "Initialization complete!"
|
||||||
|
@echo "Run 'make start' to start the LDAP server"
|
||||||
|
|
||||||
|
certs-generate: ## Generate self-signed SSL certificates
|
||||||
|
@echo "Generating SSL certificates..."
|
||||||
|
uv run python scripts/generate_certs.py
|
||||||
|
@echo "✅ Certificates generated"
|
||||||
|
|
||||||
|
certs-check: ## Check if SSL certificates exist
|
||||||
|
@echo "Checking SSL certificates..."
|
||||||
|
@if [ ! -f certs/ca.crt ] || [ ! -f certs/server.crt ] || [ ! -f certs/server.key ]; then \
|
||||||
|
echo "⚠️ Warning: SSL certificates not found"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "You can:"; \
|
||||||
|
echo " 1. Copy your dev-ca certificates to certs/"; \
|
||||||
|
echo " cp /path/to/dev-ca/ca.crt certs/"; \
|
||||||
|
echo " cp /path/to/dev-ca/server.crt certs/"; \
|
||||||
|
echo " cp /path/to/dev-ca/server.key certs/"; \
|
||||||
|
echo " 2. Generate self-signed certs: make certs-generate"; \
|
||||||
|
echo ""; \
|
||||||
|
exit 1; \
|
||||||
|
else \
|
||||||
|
echo "✅ SSL certificates found"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
start: ## Start the LDAP server
|
||||||
|
@echo "Starting LDAP server..."
|
||||||
|
docker-compose up -d
|
||||||
|
@echo "✅ LDAP server started"
|
||||||
|
@echo ""
|
||||||
|
@echo "Services available at:"
|
||||||
|
@echo " - LDAP: ldap://localhost:389"
|
||||||
|
@echo " - LDAPS: ldaps://localhost:636"
|
||||||
|
@echo " - Admin: http://localhost:8080"
|
||||||
|
@echo ""
|
||||||
|
@echo "Admin credentials:"
|
||||||
|
@echo " DN: cn=admin,dc=testing,dc=local"
|
||||||
|
@echo " Password: admin_password"
|
||||||
|
@echo ""
|
||||||
|
@echo "Run 'make logs' to view logs"
|
||||||
|
|
||||||
|
stop: ## Stop the LDAP server
|
||||||
|
@echo "Stopping LDAP server..."
|
||||||
|
docker-compose stop
|
||||||
|
@echo "✅ LDAP server stopped"
|
||||||
|
|
||||||
|
restart: ## Restart the LDAP server
|
||||||
|
@echo "Restarting LDAP server..."
|
||||||
|
docker-compose restart
|
||||||
|
@echo "✅ LDAP server restarted"
|
||||||
|
|
||||||
|
down: ## Stop and remove containers (keeps data)
|
||||||
|
@echo "Stopping and removing containers..."
|
||||||
|
docker-compose down
|
||||||
|
@echo "✅ Containers removed (data preserved)"
|
||||||
|
|
||||||
|
down-volumes: ## Stop and remove containers AND volumes (deletes all data)
|
||||||
|
@echo "⚠️ WARNING: This will delete all LDAP data!"
|
||||||
|
@read -p "Are you sure? [y/N] " -n 1 -r; \
|
||||||
|
echo; \
|
||||||
|
if [[ $$REPLY =~ ^[Yy]$$ ]]; then \
|
||||||
|
docker-compose down -v; \
|
||||||
|
echo "✅ Containers and volumes removed"; \
|
||||||
|
else \
|
||||||
|
echo "Aborted"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
logs: ## View LDAP server logs (follow mode)
|
||||||
|
docker-compose logs -f openldap
|
||||||
|
|
||||||
|
logs-tail: ## View last 100 lines of logs
|
||||||
|
docker-compose logs --tail=100 openldap
|
||||||
|
|
||||||
|
logs-admin: ## View phpLDAPadmin logs
|
||||||
|
docker-compose logs -f phpldapadmin
|
||||||
|
|
||||||
|
status: ## Show container status
|
||||||
|
@docker-compose ps
|
||||||
|
|
||||||
|
test-connection: ## Test connection to LDAP server
|
||||||
|
@echo "Testing LDAP connection..."
|
||||||
|
uv run python -c "from ldap3 import Server, Connection, ALL; s = Server('ldap://localhost:389', get_info=ALL); c = Connection(s, auto_bind=True); print('✅ Connection successful'); c.unbind()"
|
||||||
|
|
||||||
|
test-auth: ## Test authentication with admin user
|
||||||
|
@echo "Testing LDAP authentication..."
|
||||||
|
uv run python -c "from ldap3 import Server, Connection; s = Server('ldap://localhost:389'); c = Connection(s, 'cn=admin,dc=testing,dc=local', 'admin_password', auto_bind=True); print('✅ Authentication successful'); c.unbind()"
|
||||||
|
|
||||||
|
test-users: ## List all users in LDAP
|
||||||
|
@echo "Listing LDAP users..."
|
||||||
|
@uv run python -c "from ldap3 import Server, Connection; s = Server('ldap://localhost:389'); c = Connection(s, 'cn=admin,dc=testing,dc=local', 'admin_password', auto_bind=True); c.search('dc=testing,dc=local', '(objectClass=inetOrgPerson)', attributes=['uid', 'cn', 'mail']); [print(f' - {e.cn}: {e.uid} ({e.mail})') for e in c.entries]; c.unbind()"
|
||||||
|
|
||||||
|
test-ssl: ## Test SSL/TLS connection
|
||||||
|
@echo "Testing LDAPS connection..."
|
||||||
|
openssl s_client -connect localhost:636 -CAfile certs/ca.crt </dev/null
|
||||||
|
|
||||||
|
test-all: test-connection test-auth test-users ## Run all tests
|
||||||
|
|
||||||
|
shell: ## Open a shell in the LDAP container
|
||||||
|
docker-compose exec openldap bash
|
||||||
|
|
||||||
|
ldapsearch: ## Run ldapsearch command (example query)
|
||||||
|
@echo "Running ldapsearch..."
|
||||||
|
ldapsearch -H ldap://localhost:389 -x -b "dc=testing,dc=local" -D "cn=admin,dc=testing,dc=local" -w admin_password
|
||||||
|
|
||||||
|
clean: ## Clean Python build artifacts
|
||||||
|
@echo "Cleaning build artifacts..."
|
||||||
|
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||||
|
find . -type f -name "*.pyo" -delete 2>/dev/null || true
|
||||||
|
find . -type d -name "*.egg-info" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find . -type f -name ".coverage" -delete 2>/dev/null || true
|
||||||
|
rm -rf build/ dist/ .eggs/
|
||||||
|
@echo "✅ Cleaned"
|
||||||
|
|
||||||
|
clean-all: clean down-volumes ## Clean everything including Docker volumes
|
||||||
|
@echo "Cleaning certificates (keeping README)..."
|
||||||
|
find certs/ -type f ! -name "README.md" -delete 2>/dev/null || true
|
||||||
|
@echo "✅ Full cleanup complete"
|
||||||
|
|
||||||
|
dev-setup: install-dev certs-generate start ## Complete development setup
|
||||||
|
@echo ""
|
||||||
|
@echo "🎉 Development environment ready!"
|
||||||
|
@echo ""
|
||||||
|
@echo "Next steps:"
|
||||||
|
@echo " - View logs: make logs"
|
||||||
|
@echo " - Test connection: make test-connection"
|
||||||
|
@echo " - List users: make test-users"
|
||||||
|
@echo " - Open admin UI: open http://localhost:8080"
|
||||||
|
|
||||||
|
quick-start: certs-check start ## Quick start (assumes certs exist)
|
||||||
|
@echo "🚀 LDAP server is running!"
|
||||||
|
|
||||||
|
rebuild: down ## Rebuild and restart containers
|
||||||
|
@echo "Rebuilding containers..."
|
||||||
|
docker-compose up -d --build
|
||||||
|
@echo "✅ Containers rebuilt and started"
|
||||||
|
|
||||||
|
# UV-specific targets
|
||||||
|
uv-install: ## Install UV package manager
|
||||||
|
@echo "Installing UV..."
|
||||||
|
@command -v uv >/dev/null 2>&1 && echo "✅ UV already installed" || curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
|
||||||
|
uv-sync: ## Sync dependencies with UV
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
uv-update: ## Update all dependencies
|
||||||
|
uv lock --upgrade
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Docker checks
|
||||||
|
check-docker: ## Check if Docker is running
|
||||||
|
@docker version >/dev/null 2>&1 && echo "✅ Docker is running" || (echo "❌ Docker is not running. Please start Docker or Rancher Desktop." && exit 1)
|
||||||
|
|
||||||
|
check-compose: ## Check if docker-compose is available
|
||||||
|
@docker-compose version >/dev/null 2>&1 && echo "✅ docker-compose is available" || (echo "❌ docker-compose not found" && exit 1)
|
||||||
|
|
||||||
|
check-all: check-docker check-compose certs-check ## Run all checks
|
||||||
|
@echo "✅ All checks passed"
|
||||||
354
QUICKREF.md
Normal file
354
QUICKREF.md
Normal file
@@ -0,0 +1,354 @@
|
|||||||
|
# LDAP Docker Quick Reference
|
||||||
|
|
||||||
|
Quick reference for common operations and configurations.
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Complete setup
|
||||||
|
make dev-setup
|
||||||
|
|
||||||
|
# Or step by step
|
||||||
|
make install # Install dependencies
|
||||||
|
make certs-generate # Generate certificates
|
||||||
|
make start # Start server
|
||||||
|
make test-connection # Test it works
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Common Commands
|
||||||
|
|
||||||
|
### Server Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make start # Start LDAP server
|
||||||
|
make stop # Stop LDAP server
|
||||||
|
make restart # Restart LDAP server
|
||||||
|
make down # Stop and remove containers
|
||||||
|
make logs # View logs (follow mode)
|
||||||
|
make status # Check container status
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make test-connection # Test LDAP connection
|
||||||
|
make test-auth # Test authentication
|
||||||
|
make test-users # List all users
|
||||||
|
make test-all # Run all tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Certificates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make certs-generate # Generate self-signed certs
|
||||||
|
make certs-check # Verify certificates
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔑 Default Credentials
|
||||||
|
|
||||||
|
### Admin Access
|
||||||
|
|
||||||
|
- **DN:** `cn=admin,dc=testing,dc=local`
|
||||||
|
- **Password:** `admin_password`
|
||||||
|
- **Base DN:** `dc=testing,dc=local`
|
||||||
|
|
||||||
|
### phpLDAPadmin
|
||||||
|
|
||||||
|
- URL: http://localhost:8080
|
||||||
|
- Login DN: `cn=admin,dc=testing,dc=local`
|
||||||
|
- Password: `admin_password`
|
||||||
|
|
||||||
|
## 👥 Test Users
|
||||||
|
|
||||||
|
All users have password: `password123`
|
||||||
|
|
||||||
|
| Username | Full Name | Email | DN |
|
||||||
|
|----------|-----------|-------|-----|
|
||||||
|
| `admin` | Admin User | admin@testing.local | `uid=admin,ou=people,dc=testing,dc=local` |
|
||||||
|
| `jdoe` | John Doe | jdoe@testing.local | `uid=jdoe,ou=people,dc=testing,dc=local` |
|
||||||
|
| `jsmith` | Jane Smith | jsmith@testing.local | `uid=jsmith,ou=people,dc=testing,dc=local` |
|
||||||
|
| `testuser` | Test User | testuser@testing.local | `uid=testuser,ou=people,dc=testing,dc=local` |
|
||||||
|
|
||||||
|
## 🌐 Service Ports
|
||||||
|
|
||||||
|
| Service | Port | URL/Connection |
|
||||||
|
|---------|------|----------------|
|
||||||
|
| LDAP | 389 | `ldap://localhost:389` |
|
||||||
|
| LDAPS | 636 | `ldaps://localhost:636` |
|
||||||
|
| phpLDAPadmin | 8080 | `http://localhost:8080` |
|
||||||
|
|
||||||
|
## 🔍 Common LDAP Queries
|
||||||
|
|
||||||
|
### Search All Users
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ldapsearch -H ldap://localhost:389 \
|
||||||
|
-D "cn=admin,dc=testing,dc=local" \
|
||||||
|
-w admin_password \
|
||||||
|
-b "ou=people,dc=testing,dc=local" \
|
||||||
|
"(objectClass=inetOrgPerson)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Specific User
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ldapsearch -H ldap://localhost:389 \
|
||||||
|
-D "cn=admin,dc=testing,dc=local" \
|
||||||
|
-w admin_password \
|
||||||
|
-b "dc=testing,dc=local" \
|
||||||
|
"(uid=jdoe)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search All Groups
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ldapsearch -H ldap://localhost:389 \
|
||||||
|
-D "cn=admin,dc=testing,dc=local" \
|
||||||
|
-w admin_password \
|
||||||
|
-b "ou=groups,dc=testing,dc=local" \
|
||||||
|
"(objectClass=groupOfNames)"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anonymous Bind (Read-Only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ldapsearch -H ldap://localhost:389 \
|
||||||
|
-x \
|
||||||
|
-b "dc=testing,dc=local" \
|
||||||
|
"(objectClass=*)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔒 LDAPS/SSL Testing
|
||||||
|
|
||||||
|
### Test SSL Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl s_client -connect localhost:636 -CAfile certs/ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
### LDAPS Search
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ldapsearch -H ldaps://localhost:636 \
|
||||||
|
-D "cn=admin,dc=testing,dc=local" \
|
||||||
|
-w admin_password \
|
||||||
|
-b "dc=testing,dc=local"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Verify Certificate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl verify -CAfile certs/ca.crt certs/server.crt
|
||||||
|
openssl x509 -in certs/server.crt -text -noout
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐍 Python LDAP3 Examples
|
||||||
|
|
||||||
|
### Simple Connection
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ldap3 import Server, Connection
|
||||||
|
|
||||||
|
server = Server('ldap://localhost:389')
|
||||||
|
conn = Connection(server,
|
||||||
|
user='cn=admin,dc=testing,dc=local',
|
||||||
|
password='admin_password',
|
||||||
|
auto_bind=True)
|
||||||
|
print("Connected!")
|
||||||
|
conn.unbind()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Search Users
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ldap3 import Server, Connection
|
||||||
|
|
||||||
|
server = Server('ldap://localhost:389')
|
||||||
|
conn = Connection(server,
|
||||||
|
user='cn=admin,dc=testing,dc=local',
|
||||||
|
password='admin_password',
|
||||||
|
auto_bind=True)
|
||||||
|
|
||||||
|
conn.search('dc=testing,dc=local',
|
||||||
|
'(objectClass=inetOrgPerson)',
|
||||||
|
attributes=['uid', 'cn', 'mail'])
|
||||||
|
|
||||||
|
for entry in conn.entries:
|
||||||
|
print(f"{entry.cn}: {entry.mail}")
|
||||||
|
|
||||||
|
conn.unbind()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authenticate User
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ldap3 import Server, Connection
|
||||||
|
|
||||||
|
server = Server('ldap://localhost:389')
|
||||||
|
conn = Connection(server,
|
||||||
|
user='uid=jdoe,ou=people,dc=testing,dc=local',
|
||||||
|
password='password123')
|
||||||
|
|
||||||
|
if conn.bind():
|
||||||
|
print("Authentication successful!")
|
||||||
|
else:
|
||||||
|
print("Authentication failed!")
|
||||||
|
conn.unbind()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐳 Docker Commands
|
||||||
|
|
||||||
|
### View Logs
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose logs -f openldap # Follow LDAP logs
|
||||||
|
docker-compose logs --tail=100 openldap # Last 100 lines
|
||||||
|
docker-compose logs phpldapadmin # Admin UI logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Shell Access
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose exec openldap bash # Shell in LDAP container
|
||||||
|
docker ps # List running containers
|
||||||
|
docker-compose ps # List project containers
|
||||||
|
```
|
||||||
|
|
||||||
|
### Volume Management
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker volume ls # List volumes
|
||||||
|
docker-compose down -v # Remove volumes (deletes data!)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Troubleshooting Quick Fixes
|
||||||
|
|
||||||
|
### Server Won't Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if ports are in use
|
||||||
|
lsof -i :389
|
||||||
|
lsof -i :636
|
||||||
|
lsof -i :8080
|
||||||
|
|
||||||
|
# Check Docker is running
|
||||||
|
docker version
|
||||||
|
|
||||||
|
# View error logs
|
||||||
|
docker-compose logs openldap
|
||||||
|
```
|
||||||
|
|
||||||
|
### Certificate Errors
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify certificates exist
|
||||||
|
ls -la certs/
|
||||||
|
|
||||||
|
# Regenerate certificates
|
||||||
|
make certs-generate --force
|
||||||
|
|
||||||
|
# Check certificate validity
|
||||||
|
openssl x509 -in certs/server.crt -noout -dates
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Refused
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check container is running
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# Wait for initialization (can take 10-30 seconds)
|
||||||
|
make logs
|
||||||
|
|
||||||
|
# Restart server
|
||||||
|
make restart
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Fails
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify credentials
|
||||||
|
# Default: cn=admin,dc=testing,dc=local / admin_password
|
||||||
|
|
||||||
|
# Check if users are loaded
|
||||||
|
make test-users
|
||||||
|
|
||||||
|
# View LDAP directory structure
|
||||||
|
ldapsearch -H ldap://localhost:389 -x -b "dc=testing,dc=local" -s base
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Not Appearing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if LDIF files were loaded
|
||||||
|
docker-compose logs openldap | grep -i ldif
|
||||||
|
|
||||||
|
# Rebuild with fresh data
|
||||||
|
make down-volumes # WARNING: Deletes all data!
|
||||||
|
make start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 File Locations
|
||||||
|
|
||||||
|
### Configuration Files
|
||||||
|
|
||||||
|
- `docker-compose.yml` - Docker services configuration
|
||||||
|
- `pyproject.toml` - Python dependencies
|
||||||
|
- `.env.example` - Environment variables template
|
||||||
|
|
||||||
|
### Data Files
|
||||||
|
|
||||||
|
- `ldif/01-users.ldif` - Initial LDAP data
|
||||||
|
- `certs/` - SSL certificates (git-ignored)
|
||||||
|
|
||||||
|
### Scripts
|
||||||
|
|
||||||
|
- `scripts/cli.py` - CLI management tool
|
||||||
|
- `scripts/generate_certs.py` - Certificate generator
|
||||||
|
- `quickstart.sh` - Interactive setup script
|
||||||
|
|
||||||
|
## 🎓 LDAP Basics
|
||||||
|
|
||||||
|
### DN (Distinguished Name)
|
||||||
|
|
||||||
|
Format: `attribute=value,ou=unit,dc=domain,dc=tld`
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- `cn=admin,dc=testing,dc=local` - Admin user
|
||||||
|
- `uid=jdoe,ou=people,dc=testing,dc=local` - Regular user
|
||||||
|
- `cn=developers,ou=groups,dc=testing,dc=local` - Group
|
||||||
|
|
||||||
|
### Common Object Classes
|
||||||
|
|
||||||
|
- `inetOrgPerson` - Person with internet attributes
|
||||||
|
- `posixAccount` - Unix/Linux account
|
||||||
|
- `groupOfNames` - Group with members
|
||||||
|
|
||||||
|
### Common Attributes
|
||||||
|
|
||||||
|
- `uid` - User ID (username)
|
||||||
|
- `cn` - Common Name (full name)
|
||||||
|
- `sn` - Surname (last name)
|
||||||
|
- `mail` - Email address
|
||||||
|
- `userPassword` - Hashed password
|
||||||
|
- `member` - Group member DN
|
||||||
|
|
||||||
|
## 🔗 Useful Links
|
||||||
|
|
||||||
|
- [OpenLDAP Documentation](https://www.openldap.org/doc/)
|
||||||
|
- [LDAP3 Python Library](https://ldap3.readthedocs.io/)
|
||||||
|
- [RFC 4511 - LDAP Protocol](https://tools.ietf.org/html/rfc4511)
|
||||||
|
- [phpLDAPadmin](http://phpldapadmin.sourceforge.net/)
|
||||||
|
|
||||||
|
## 💡 Tips
|
||||||
|
|
||||||
|
1. **Use LDAPS in applications**: Always prefer `ldaps://` over `ldap://`
|
||||||
|
2. **Test with anonymous bind first**: Use `-x` flag with ldapsearch
|
||||||
|
3. **Check logs when troubleshooting**: `make logs` is your friend
|
||||||
|
4. **Certificate hostname must match**: Ensure SAN includes `ldap.testing.local`
|
||||||
|
5. **Wait after starting**: Server needs 10-30 seconds to initialize
|
||||||
|
6. **Backup before experimenting**: Use `make down` not `make down-volumes`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Need more help?** See full documentation in README.md
|
||||||
459
README.md
Normal file
459
README.md
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
# LDAP Docker
|
||||||
|
|
||||||
|
This is a development tool project that, when deployed, offers an SSL-capable OpenLDAP server populated with test users for a fictional `testing.local` domain.
|
||||||
|
|
||||||
|
Perfect for local development and testing of applications that need LDAP authentication without setting up a complex infrastructure.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔒 **SSL/TLS Support** - LDAPS on port 636 with custom certificate support
|
||||||
|
- 👥 **Pre-populated Users** - Test users and groups ready to use
|
||||||
|
- 🌐 **Web Admin Interface** - phpLDAPadmin for easy management
|
||||||
|
- 🐳 **Docker-based** - Easy deployment with Docker Compose
|
||||||
|
- 🔧 **Python Management Tools** - UV-based CLI for convenience
|
||||||
|
- 🍎 **Cross-platform** - Works on MacOS (Rancher Desktop), Linux, and Windows
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Quick Start](#quick-start)
|
||||||
|
- [Custom Certificates (Dev-CA)](#custom-certificates-dev-ca)
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [Test Users](#test-users)
|
||||||
|
- [Management Tools](#management-tools)
|
||||||
|
- [Project Structure](#project-structure)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Development](#development)
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
### Required
|
||||||
|
|
||||||
|
- **Docker** or **Rancher Desktop** (recommended for MacOS)
|
||||||
|
- MacOS: [Download Rancher Desktop](https://rancherdesktop.io/)
|
||||||
|
- Alternatively: [Docker Desktop](https://www.docker.com/products/docker-desktop)
|
||||||
|
- **Python 3.9+** (for management scripts)
|
||||||
|
- **UV Package Manager** (recommended)
|
||||||
|
```bash
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional
|
||||||
|
|
||||||
|
- **OpenSSL** (for certificate verification)
|
||||||
|
- **LDAP utilities** (ldapsearch, ldapadd, etc.) for testing
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Option 1: Using Make (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Install dependencies
|
||||||
|
make install
|
||||||
|
|
||||||
|
# 2. Generate certificates (or copy your own - see below)
|
||||||
|
make certs-generate
|
||||||
|
|
||||||
|
# 3. Start the server
|
||||||
|
make start
|
||||||
|
|
||||||
|
# 4. Test the connection
|
||||||
|
make test-connection
|
||||||
|
|
||||||
|
# 5. List users
|
||||||
|
make test-users
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Using Docker Compose Directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Generate certificates
|
||||||
|
python scripts/generate_certs.py
|
||||||
|
|
||||||
|
# 2. Start services
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# 3. View logs
|
||||||
|
docker-compose logs -f openldap
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 3: Complete Dev Setup
|
||||||
|
|
||||||
|
This will install everything, generate certificates, and start the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make dev-setup
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Certificates (Dev-CA)
|
||||||
|
|
||||||
|
If you maintain your own dev-ca (as mentioned), you can use your own certificates instead of self-signed ones:
|
||||||
|
|
||||||
|
### Using Your Dev-CA Certificates
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy your certificates to the certs directory
|
||||||
|
cp /path/to/your/dev-ca/certs/ldap-server.crt certs/server.crt
|
||||||
|
cp /path/to/your/dev-ca/private/ldap-server.key certs/server.key
|
||||||
|
cp /path/to/your/dev-ca/ca-cert.pem certs/ca.crt
|
||||||
|
|
||||||
|
# Ensure proper permissions
|
||||||
|
chmod 644 certs/ca.crt
|
||||||
|
chmod 644 certs/server.crt
|
||||||
|
chmod 600 certs/server.key
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** The server certificate should be issued for the hostname `ldap.testing.local` or include it as a Subject Alternative Name (SAN).
|
||||||
|
|
||||||
|
### Certificate Requirements
|
||||||
|
|
||||||
|
The OpenLDAP container expects three files in the `certs/` directory:
|
||||||
|
|
||||||
|
- `ca.crt` - Your CA root certificate
|
||||||
|
- `server.crt` - Server certificate for ldap.testing.local
|
||||||
|
- `server.key` - Private key for the server certificate
|
||||||
|
|
||||||
|
### Generating Certificates with Your OpenSSL-based Dev-CA
|
||||||
|
|
||||||
|
If your dev-ca is script-based and uses OpenSSL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example using your dev-ca
|
||||||
|
cd /path/to/your/dev-ca
|
||||||
|
|
||||||
|
# Generate server key
|
||||||
|
openssl genrsa -out ldap-server.key 4096
|
||||||
|
|
||||||
|
# Generate certificate signing request
|
||||||
|
openssl req -new -key ldap-server.key -out ldap-server.csr \
|
||||||
|
-subj "/CN=ldap.testing.local"
|
||||||
|
|
||||||
|
# Sign with your CA (adjust paths as needed)
|
||||||
|
openssl x509 -req -in ldap-server.csr \
|
||||||
|
-CA ca-cert.pem -CAkey ca-key.pem \
|
||||||
|
-CAcreateserial -out ldap-server.crt \
|
||||||
|
-days 365 -sha256 \
|
||||||
|
-extfile <(printf "subjectAltName=DNS:ldap.testing.local,DNS:localhost,IP:127.0.0.1")
|
||||||
|
|
||||||
|
# Copy to LDAP Docker project
|
||||||
|
cp ldap-server.crt /path/to/ldap_docker/certs/server.crt
|
||||||
|
cp ldap-server.key /path/to/ldap_docker/certs/server.key
|
||||||
|
cp ca-cert.pem /path/to/ldap_docker/certs/ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Accessing the Services
|
||||||
|
|
||||||
|
Once started, the following services are available:
|
||||||
|
|
||||||
|
| Service | URL/Connection | Description |
|
||||||
|
|---------|----------------|-------------|
|
||||||
|
| LDAP | `ldap://localhost:389` | Standard LDAP (unencrypted) |
|
||||||
|
| LDAPS | `ldaps://localhost:636` | LDAP over SSL/TLS |
|
||||||
|
| phpLDAPadmin | `http://localhost:8080` | Web-based administration |
|
||||||
|
|
||||||
|
### Admin Credentials
|
||||||
|
|
||||||
|
- **Admin DN:** `cn=admin,dc=testing,dc=local`
|
||||||
|
- **Password:** `admin_password`
|
||||||
|
- **Base DN:** `dc=testing,dc=local`
|
||||||
|
|
||||||
|
### Common Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start the server
|
||||||
|
make start
|
||||||
|
|
||||||
|
# Stop the server
|
||||||
|
make stop
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
make logs
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
make status
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test-all
|
||||||
|
|
||||||
|
# Open shell in container
|
||||||
|
make shell
|
||||||
|
|
||||||
|
# Clean up everything
|
||||||
|
make clean-all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Users
|
||||||
|
|
||||||
|
The LDAP directory is pre-populated with the following test users:
|
||||||
|
|
||||||
|
| Username | Full Name | Email | Password | UID |
|
||||||
|
|----------|-----------|-------|----------|-----|
|
||||||
|
| admin | Admin User | admin@testing.local | password123 | 10000 |
|
||||||
|
| jdoe | John Doe | jdoe@testing.local | password123 | 10001 |
|
||||||
|
| jsmith | Jane Smith | jsmith@testing.local | password123 | 10002 |
|
||||||
|
| testuser | Test User | testuser@testing.local | password123 | 10003 |
|
||||||
|
|
||||||
|
### Test Groups
|
||||||
|
|
||||||
|
- **admins** - Administrator group (member: admin)
|
||||||
|
- **developers** - Development team (members: jdoe, jsmith)
|
||||||
|
- **users** - General users (members: jdoe, jsmith, testuser)
|
||||||
|
|
||||||
|
### Testing Authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test with ldapsearch
|
||||||
|
ldapsearch -H ldap://localhost:389 \
|
||||||
|
-D "uid=jdoe,ou=people,dc=testing,dc=local" \
|
||||||
|
-w password123 \
|
||||||
|
-b "dc=testing,dc=local" \
|
||||||
|
"(uid=jdoe)"
|
||||||
|
|
||||||
|
# Test LDAPS with SSL
|
||||||
|
ldapsearch -H ldaps://localhost:636 \
|
||||||
|
-D "uid=jdoe,ou=people,dc=testing,dc=local" \
|
||||||
|
-w password123 \
|
||||||
|
-b "dc=testing,dc=local" \
|
||||||
|
"(uid=jdoe)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Management Tools
|
||||||
|
|
||||||
|
This project includes Python-based management tools using UV.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies with UV
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Or manually
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
### CLI Tool Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# View available commands
|
||||||
|
uv run ldap-docker --help
|
||||||
|
|
||||||
|
# Server management
|
||||||
|
uv run ldap-docker server start
|
||||||
|
uv run ldap-docker server stop
|
||||||
|
uv run ldap-docker server logs -f
|
||||||
|
|
||||||
|
# Certificate management
|
||||||
|
uv run ldap-docker certs generate
|
||||||
|
uv run ldap-docker certs check
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
uv run ldap-docker test connection
|
||||||
|
uv run ldap-docker test auth
|
||||||
|
uv run ldap-docker test users
|
||||||
|
|
||||||
|
# Initialize environment
|
||||||
|
uv run ldap-docker init
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
ldap_docker/
|
||||||
|
├── certs/ # SSL/TLS certificates (git-ignored)
|
||||||
|
│ ├── README.md # Certificate documentation
|
||||||
|
│ ├── ca.crt # CA certificate (your dev-ca)
|
||||||
|
│ ├── server.crt # Server certificate
|
||||||
|
│ └── server.key # Server private key
|
||||||
|
├── ldif/ # LDAP Data Interchange Format files
|
||||||
|
│ └── 01-users.ldif # Initial user and group data
|
||||||
|
├── scripts/ # Management scripts
|
||||||
|
│ ├── cli.py # CLI tool for managing LDAP
|
||||||
|
│ └── generate_certs.py # Certificate generation utility
|
||||||
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
|
├── pyproject.toml # Python project configuration (UV)
|
||||||
|
├── Makefile # Convenient command shortcuts
|
||||||
|
├── .gitignore # Git ignore rules
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Docker/Rancher Desktop Issues
|
||||||
|
|
||||||
|
**Problem:** `docker` command not found
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Rancher Desktop on MacOS, ensure it's running and configured
|
||||||
|
# Check Docker settings in Rancher Desktop preferences
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Cannot connect to Docker daemon
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ensure Rancher Desktop or Docker Desktop is running
|
||||||
|
# On MacOS: Check if Rancher Desktop is in the menu bar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Certificate Issues
|
||||||
|
|
||||||
|
**Problem:** LDAPS connection fails with certificate error
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify certificates exist
|
||||||
|
make certs-check
|
||||||
|
|
||||||
|
# Check certificate details
|
||||||
|
openssl x509 -in certs/server.crt -text -noout
|
||||||
|
|
||||||
|
# Verify certificate chain
|
||||||
|
openssl verify -CAfile certs/ca.crt certs/server.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Wrong hostname in certificate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Regenerate with correct hostname
|
||||||
|
make certs-generate
|
||||||
|
|
||||||
|
# Or copy certificates from your dev-ca with correct hostname
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Issues
|
||||||
|
|
||||||
|
**Problem:** Cannot connect to LDAP server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if containers are running
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
# View logs for errors
|
||||||
|
make logs
|
||||||
|
|
||||||
|
# Test basic connectivity
|
||||||
|
telnet localhost 389
|
||||||
|
```
|
||||||
|
|
||||||
|
**Problem:** Authentication fails
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify credentials
|
||||||
|
# Default admin: cn=admin,dc=testing,dc=local / admin_password
|
||||||
|
|
||||||
|
# Check LDAP logs
|
||||||
|
docker-compose logs openldap | grep -i error
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Issues
|
||||||
|
|
||||||
|
**Problem:** Users not appearing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if LDIF files were loaded
|
||||||
|
docker-compose logs openldap | grep -i ldif
|
||||||
|
|
||||||
|
# Restart and reload
|
||||||
|
make down-volumes # WARNING: Deletes data
|
||||||
|
make start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### Adding Custom Users
|
||||||
|
|
||||||
|
Edit `ldif/01-users.ldif` to add more users or modify existing ones:
|
||||||
|
|
||||||
|
```ldif
|
||||||
|
dn: uid=newuser,ou=people,dc=testing,dc=local
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: posixAccount
|
||||||
|
objectClass: shadowAccount
|
||||||
|
uid: newuser
|
||||||
|
cn: New User
|
||||||
|
sn: User
|
||||||
|
mail: newuser@testing.local
|
||||||
|
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ=
|
||||||
|
uidNumber: 10004
|
||||||
|
gidNumber: 10004
|
||||||
|
homeDirectory: /home/newuser
|
||||||
|
loginShell: /bin/bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Then restart with fresh data:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make down-volumes
|
||||||
|
make start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modifying Configuration
|
||||||
|
|
||||||
|
Edit `docker-compose.yml` to change:
|
||||||
|
- Port mappings
|
||||||
|
- Environment variables
|
||||||
|
- Volume mounts
|
||||||
|
- Resource limits
|
||||||
|
|
||||||
|
### Python Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install development dependencies
|
||||||
|
make install-dev
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
uv run pytest
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
uv run black scripts/
|
||||||
|
|
||||||
|
# Lint code
|
||||||
|
uv run ruff check scripts/
|
||||||
|
|
||||||
|
# Type check
|
||||||
|
uv run mypy scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building on Other Platforms
|
||||||
|
|
||||||
|
This project is designed to work on:
|
||||||
|
- **MacOS** (with Rancher Desktop or Docker Desktop)
|
||||||
|
- **Linux** (with Docker and Docker Compose)
|
||||||
|
- **Windows** (with Docker Desktop and WSL2)
|
||||||
|
|
||||||
|
The `docker-compose.yml` uses standard Docker images and should work anywhere Docker runs.
|
||||||
|
|
||||||
|
## Security Notes
|
||||||
|
|
||||||
|
⚠️ **This is a development tool only!**
|
||||||
|
|
||||||
|
- Default passwords are weak and well-known
|
||||||
|
- Self-signed certificates are not trusted
|
||||||
|
- No backup or disaster recovery
|
||||||
|
- No monitoring or alerting
|
||||||
|
- Not hardened for production use
|
||||||
|
|
||||||
|
**Never use this in production or with real user data!**
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License - See LICENSE file for details
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! This is a development tool, so:
|
||||||
|
1. Keep it simple and easy to use
|
||||||
|
2. Maintain cross-platform compatibility
|
||||||
|
3. Update documentation for any changes
|
||||||
|
4. Test with both Docker Desktop and Rancher Desktop
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [OpenLDAP Documentation](https://www.openldap.org/doc/)
|
||||||
|
- [LDAP on Docker Hub](https://hub.docker.com/r/osixia/openldap)
|
||||||
|
- [UV Package Manager](https://github.com/astral-sh/uv)
|
||||||
|
- [LDAP Tools Guide](https://www.openldap.org/doc/admin24/quickstart.html)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Need Help?** Check the [Troubleshooting](#troubleshooting) section or view logs with `make logs`
|
||||||
134
certs/README.md
Normal file
134
certs/README.md
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
# SSL/TLS Certificates for LDAP
|
||||||
|
|
||||||
|
This directory should contain your SSL/TLS certificates for the LDAP server.
|
||||||
|
|
||||||
|
## Required Files
|
||||||
|
|
||||||
|
The OpenLDAP container expects the following files in this directory:
|
||||||
|
|
||||||
|
- `ca.crt` - Certificate Authority certificate (your dev-ca root certificate)
|
||||||
|
- `server.crt` - Server certificate for ldap.testing.local
|
||||||
|
- `server.key` - Private key for the server certificate
|
||||||
|
|
||||||
|
## Using Your Custom Dev-CA Certificates
|
||||||
|
|
||||||
|
If you maintain your own dev-ca (as mentioned), simply copy your certificates here:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy your dev-ca generated certificates to this directory
|
||||||
|
cp /path/to/your/dev-ca/certs/ldap-server.crt ./server.crt
|
||||||
|
cp /path/to/your/dev-ca/private/ldap-server.key ./server.key
|
||||||
|
cp /path/to/your/dev-ca/ca-cert.pem ./ca.crt
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- The server certificate should be issued for the hostname `ldap.testing.local`
|
||||||
|
- The certificate can also include SANs (Subject Alternative Names) like:
|
||||||
|
- `DNS:ldap.testing.local`
|
||||||
|
- `DNS:localhost`
|
||||||
|
- `IP:127.0.0.1`
|
||||||
|
- Ensure the private key is readable by the container (permissions should be 600 or 644)
|
||||||
|
|
||||||
|
## Generating Self-Signed Certificates (Quick Start)
|
||||||
|
|
||||||
|
If you don't have your dev-ca handy and want to quickly test, you can generate self-signed certificates:
|
||||||
|
|
||||||
|
### Option 1: Using OpenSSL (Manual)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate CA private key
|
||||||
|
openssl genrsa -out ca.key 4096
|
||||||
|
|
||||||
|
# Generate CA certificate
|
||||||
|
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
|
||||||
|
-subj "/C=US/ST=State/L=City/O=Testing Org/CN=Testing CA"
|
||||||
|
|
||||||
|
# Generate server private key
|
||||||
|
openssl genrsa -out server.key 4096
|
||||||
|
|
||||||
|
# Generate server certificate signing request
|
||||||
|
openssl req -new -key server.key -out server.csr \
|
||||||
|
-subj "/C=US/ST=State/L=City/O=Testing Org/CN=ldap.testing.local"
|
||||||
|
|
||||||
|
# Create extensions file for SAN
|
||||||
|
cat > server-ext.cnf <<EOF
|
||||||
|
subjectAltName = DNS:ldap.testing.local,DNS:localhost,IP:127.0.0.1
|
||||||
|
extendedKeyUsage = serverAuth,clientAuth
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Sign the server certificate with the CA
|
||||||
|
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
|
||||||
|
-CAcreateserial -out server.crt -days 365 \
|
||||||
|
-extfile server-ext.cnf
|
||||||
|
|
||||||
|
# Clean up temporary files
|
||||||
|
rm server.csr ca.key server-ext.cnf ca.srl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: Using the Provided Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the certificate generation script from the project root
|
||||||
|
python scripts/generate_certs.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Permissions
|
||||||
|
|
||||||
|
Ensure proper permissions for security:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 644 ca.crt
|
||||||
|
chmod 644 server.crt
|
||||||
|
chmod 600 server.key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifying Your Certificates
|
||||||
|
|
||||||
|
After placing your certificates, verify them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check certificate details
|
||||||
|
openssl x509 -in server.crt -text -noout
|
||||||
|
|
||||||
|
# Verify certificate chain
|
||||||
|
openssl verify -CAfile ca.crt server.crt
|
||||||
|
|
||||||
|
# Check certificate and key match
|
||||||
|
openssl x509 -noout -modulus -in server.crt | openssl md5
|
||||||
|
openssl rsa -noout -modulus -in server.key | openssl md5
|
||||||
|
# The MD5 hashes should match
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing LDAPS Connection
|
||||||
|
|
||||||
|
Once the container is running with your certificates:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test LDAPS connection (port 636)
|
||||||
|
openssl s_client -connect localhost:636 -CAfile certs/ca.crt
|
||||||
|
|
||||||
|
# Test with ldapsearch
|
||||||
|
ldapsearch -H ldaps://localhost:636 -x -b "dc=testing,dc=local" \
|
||||||
|
-D "cn=admin,dc=testing,dc=local" -w admin_password
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Certificate Errors
|
||||||
|
|
||||||
|
If you see TLS/SSL errors in the logs:
|
||||||
|
1. Verify the certificate hostname matches `ldap.testing.local`
|
||||||
|
2. Check that all three files are present and readable
|
||||||
|
3. Ensure the server certificate is signed by the CA
|
||||||
|
4. Check certificate expiration dates
|
||||||
|
|
||||||
|
### Container Won't Start
|
||||||
|
|
||||||
|
If the container fails to start:
|
||||||
|
1. Check Docker logs: `docker-compose logs openldap`
|
||||||
|
2. Verify file permissions on certificate files
|
||||||
|
3. Ensure certificates are in PEM format (not DER or other formats)
|
||||||
|
|
||||||
|
## Security Note
|
||||||
|
|
||||||
|
These certificates are for **development use only**. Never use self-signed or development certificates in production environments.
|
||||||
75
docker-compose.yml
Normal file
75
docker-compose.yml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
openldap:
|
||||||
|
image: osixia/openldap:1.5.0
|
||||||
|
container_name: ldap-server
|
||||||
|
hostname: ldap.testing.local
|
||||||
|
environment:
|
||||||
|
# Base domain configuration
|
||||||
|
LDAP_ORGANISATION: "Testing Organization"
|
||||||
|
LDAP_DOMAIN: "testing.local"
|
||||||
|
LDAP_BASE_DN: "dc=testing,dc=local"
|
||||||
|
|
||||||
|
# Admin credentials (change these for production)
|
||||||
|
LDAP_ADMIN_PASSWORD: "admin_password"
|
||||||
|
LDAP_CONFIG_PASSWORD: "config_password"
|
||||||
|
|
||||||
|
# SSL/TLS Configuration
|
||||||
|
LDAP_TLS: "true"
|
||||||
|
LDAP_TLS_CRT_FILENAME: "server.crt"
|
||||||
|
LDAP_TLS_KEY_FILENAME: "server.key"
|
||||||
|
LDAP_TLS_CA_CRT_FILENAME: "ca.crt"
|
||||||
|
LDAP_TLS_VERIFY_CLIENT: "try"
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
LDAP_LOG_LEVEL: "256"
|
||||||
|
|
||||||
|
ports:
|
||||||
|
# Standard LDAP port
|
||||||
|
- "389:389"
|
||||||
|
# LDAPS (SSL) port
|
||||||
|
- "636:636"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Custom certificates - place your dev-ca certs here
|
||||||
|
- ./certs:/container/service/slapd/assets/certs:ro
|
||||||
|
|
||||||
|
# LDIF files for initial data population
|
||||||
|
- ./ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom:ro
|
||||||
|
|
||||||
|
# Persistent data storage
|
||||||
|
- ldap_data:/var/lib/ldap
|
||||||
|
- ldap_config:/etc/ldap/slapd.d
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- ldap-network
|
||||||
|
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
command: --copy-service --loglevel debug
|
||||||
|
|
||||||
|
# Optional: phpLDAPadmin for web-based management
|
||||||
|
phpldapadmin:
|
||||||
|
image: osixia/phpldapadmin:0.9.0
|
||||||
|
container_name: ldap-admin
|
||||||
|
environment:
|
||||||
|
PHPLDAPADMIN_LDAP_HOSTS: "openldap"
|
||||||
|
PHPLDAPADMIN_HTTPS: "false"
|
||||||
|
ports:
|
||||||
|
- "8080:80"
|
||||||
|
depends_on:
|
||||||
|
- openldap
|
||||||
|
networks:
|
||||||
|
- ldap-network
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
ldap_data:
|
||||||
|
driver: local
|
||||||
|
ldap_config:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
networks:
|
||||||
|
ldap-network:
|
||||||
|
driver: bridge
|
||||||
336
examples/README.md
Normal file
336
examples/README.md
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
# LDAP Docker Examples
|
||||||
|
|
||||||
|
This directory contains example scripts and applications demonstrating how to use the LDAP server for authentication and user management.
|
||||||
|
|
||||||
|
## Available Examples
|
||||||
|
|
||||||
|
### 1. Simple Authentication (`simple_auth.py`)
|
||||||
|
|
||||||
|
A Python script demonstrating basic LDAP authentication and user information retrieval.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Authenticate users with username/password
|
||||||
|
- Retrieve detailed user information
|
||||||
|
- Get user group memberships
|
||||||
|
- List all users in the directory
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Authenticate a user (default: jdoe)
|
||||||
|
python examples/simple_auth.py
|
||||||
|
|
||||||
|
# Authenticate with custom credentials
|
||||||
|
python examples/simple_auth.py --username jsmith --password password123
|
||||||
|
|
||||||
|
# List all users
|
||||||
|
python examples/simple_auth.py --list-users
|
||||||
|
|
||||||
|
# Use a different LDAP server
|
||||||
|
python examples/simple_auth.py --server ldaps://localhost:636
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example Output:**
|
||||||
|
|
||||||
|
```
|
||||||
|
🔐 LDAP Authentication Example
|
||||||
|
Server: ldap://localhost:389
|
||||||
|
|
||||||
|
Attempting to authenticate user: jdoe
|
||||||
|
✅ Authentication successful for user: jdoe
|
||||||
|
✅ Authentication successful!
|
||||||
|
|
||||||
|
Fetching user information...
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
USER INFORMATION
|
||||||
|
==================================================
|
||||||
|
Username: jdoe
|
||||||
|
Full Name: John Doe
|
||||||
|
First Name: John
|
||||||
|
Last Name: Doe
|
||||||
|
Email: jdoe@testing.local
|
||||||
|
UID Number: 10001
|
||||||
|
GID Number: 10001
|
||||||
|
DN: uid=jdoe,ou=people,dc=testing,dc=local
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Fetching user groups...
|
||||||
|
User belongs to 2 group(s):
|
||||||
|
• developers
|
||||||
|
• users
|
||||||
|
```
|
||||||
|
|
||||||
|
## Using in Your Application
|
||||||
|
|
||||||
|
### Python with ldap3
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ldap3 import Server, Connection
|
||||||
|
|
||||||
|
# Connect and authenticate
|
||||||
|
server = Server('ldap://localhost:389')
|
||||||
|
conn = Connection(
|
||||||
|
server,
|
||||||
|
user='uid=jdoe,ou=people,dc=testing,dc=local',
|
||||||
|
password='password123',
|
||||||
|
auto_bind=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Search for users
|
||||||
|
conn.search(
|
||||||
|
'dc=testing,dc=local',
|
||||||
|
'(objectClass=inetOrgPerson)',
|
||||||
|
attributes=['uid', 'cn', 'mail']
|
||||||
|
)
|
||||||
|
|
||||||
|
for entry in conn.entries:
|
||||||
|
print(f"{entry.cn}: {entry.mail}")
|
||||||
|
|
||||||
|
conn.unbind()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using ldapsearch (Command Line)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Search for a user
|
||||||
|
ldapsearch -H ldap://localhost:389 \
|
||||||
|
-D "cn=admin,dc=testing,dc=local" \
|
||||||
|
-w admin_password \
|
||||||
|
-b "dc=testing,dc=local" \
|
||||||
|
"(uid=jdoe)"
|
||||||
|
|
||||||
|
# List all users
|
||||||
|
ldapsearch -H ldap://localhost:389 \
|
||||||
|
-D "cn=admin,dc=testing,dc=local" \
|
||||||
|
-w admin_password \
|
||||||
|
-b "ou=people,dc=testing,dc=local" \
|
||||||
|
"(objectClass=inetOrgPerson)" \
|
||||||
|
uid cn mail
|
||||||
|
```
|
||||||
|
|
||||||
|
### Web Application Integration
|
||||||
|
|
||||||
|
#### Flask Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from flask import Flask, request, jsonify
|
||||||
|
from ldap3 import Server, Connection
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route('/login', methods=['POST'])
|
||||||
|
def login():
|
||||||
|
username = request.json.get('username')
|
||||||
|
password = request.json.get('password')
|
||||||
|
|
||||||
|
server = Server('ldap://localhost:389')
|
||||||
|
user_dn = f'uid={username},ou=people,dc=testing,dc=local'
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = Connection(server, user=user_dn, password=password)
|
||||||
|
if conn.bind():
|
||||||
|
return jsonify({'status': 'success', 'message': 'Authenticated'})
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Invalid credentials'}), 401
|
||||||
|
except:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Authentication failed'}), 401
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Django Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
# settings.py
|
||||||
|
import ldap
|
||||||
|
from django_auth_ldap.config import LDAPSearch
|
||||||
|
|
||||||
|
AUTH_LDAP_SERVER_URI = "ldap://localhost:389"
|
||||||
|
AUTH_LDAP_BIND_DN = "cn=admin,dc=testing,dc=local"
|
||||||
|
AUTH_LDAP_BIND_PASSWORD = "admin_password"
|
||||||
|
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||||
|
"ou=people,dc=testing,dc=local",
|
||||||
|
ldap.SCOPE_SUBTREE,
|
||||||
|
"(uid=%(user)s)"
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
'django_auth_ldap.backend.LDAPBackend',
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Integration Patterns
|
||||||
|
|
||||||
|
### 1. Simple Bind Authentication
|
||||||
|
|
||||||
|
The most straightforward approach - try to bind with user credentials:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def authenticate_user(username, password):
|
||||||
|
server = Server('ldap://localhost:389')
|
||||||
|
user_dn = f'uid={username},ou=people,dc=testing,dc=local'
|
||||||
|
conn = Connection(server, user=user_dn, password=password)
|
||||||
|
return conn.bind()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Search and Bind
|
||||||
|
|
||||||
|
Search for the user first, then authenticate:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def authenticate_user(username, password):
|
||||||
|
# First, search for the user with admin credentials
|
||||||
|
server = Server('ldap://localhost:389')
|
||||||
|
admin_conn = Connection(
|
||||||
|
server,
|
||||||
|
user='cn=admin,dc=testing,dc=local',
|
||||||
|
password='admin_password',
|
||||||
|
auto_bind=True
|
||||||
|
)
|
||||||
|
|
||||||
|
admin_conn.search(
|
||||||
|
'ou=people,dc=testing,dc=local',
|
||||||
|
f'(uid={username})',
|
||||||
|
attributes=['dn']
|
||||||
|
)
|
||||||
|
|
||||||
|
if not admin_conn.entries:
|
||||||
|
return False
|
||||||
|
|
||||||
|
user_dn = admin_conn.entries[0].entry_dn
|
||||||
|
admin_conn.unbind()
|
||||||
|
|
||||||
|
# Now authenticate as the user
|
||||||
|
user_conn = Connection(server, user=user_dn, password=password)
|
||||||
|
return user_conn.bind()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Group-Based Authorization
|
||||||
|
|
||||||
|
Check if user belongs to specific groups:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def user_has_role(username, required_group):
|
||||||
|
server = Server('ldap://localhost:389')
|
||||||
|
conn = Connection(
|
||||||
|
server,
|
||||||
|
user='cn=admin,dc=testing,dc=local',
|
||||||
|
password='admin_password',
|
||||||
|
auto_bind=True
|
||||||
|
)
|
||||||
|
|
||||||
|
user_dn = f'uid={username},ou=people,dc=testing,dc=local'
|
||||||
|
|
||||||
|
conn.search(
|
||||||
|
'ou=groups,dc=testing,dc=local',
|
||||||
|
f'(&(objectClass=groupOfNames)(member={user_dn})(cn={required_group}))',
|
||||||
|
attributes=['cn']
|
||||||
|
)
|
||||||
|
|
||||||
|
return len(conn.entries) > 0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Your Integration
|
||||||
|
|
||||||
|
### 1. Start the LDAP Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make start
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python examples/simple_auth.py --list-users
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Test Authentication
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python examples/simple_auth.py --username jdoe --password password123
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Test with Your Application
|
||||||
|
|
||||||
|
Point your application to:
|
||||||
|
- LDAP URL: `ldap://localhost:389`
|
||||||
|
- LDAPS URL: `ldaps://localhost:636` (with SSL)
|
||||||
|
- Base DN: `dc=testing,dc=local`
|
||||||
|
|
||||||
|
## Available Test Accounts
|
||||||
|
|
||||||
|
| Username | Password | Groups | Purpose |
|
||||||
|
|----------|----------|--------|---------|
|
||||||
|
| admin | password123 | admins | Administrative testing |
|
||||||
|
| jdoe | password123 | developers, users | Regular user testing |
|
||||||
|
| jsmith | password123 | developers, users | Regular user testing |
|
||||||
|
| testuser | password123 | users | Basic user testing |
|
||||||
|
|
||||||
|
## SSL/TLS Configuration
|
||||||
|
|
||||||
|
For production-like testing with LDAPS:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import ssl
|
||||||
|
from ldap3 import Server, Connection, Tls
|
||||||
|
|
||||||
|
tls = Tls(
|
||||||
|
ca_certs_file='certs/ca.crt',
|
||||||
|
validate=ssl.CERT_REQUIRED
|
||||||
|
)
|
||||||
|
|
||||||
|
server = Server('ldaps://localhost:636', use_ssl=True, tls=tls)
|
||||||
|
conn = Connection(server, user=user_dn, password=password, auto_bind=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Connection Refused
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check if LDAP server is running
|
||||||
|
make status
|
||||||
|
|
||||||
|
# Start if not running
|
||||||
|
make start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication Fails
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verify user exists
|
||||||
|
make test-users
|
||||||
|
|
||||||
|
# Check LDAP logs
|
||||||
|
make logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Python ImportError
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install ldap3 library
|
||||||
|
uv pip install ldap3
|
||||||
|
# or
|
||||||
|
pip install ldap3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- [ldap3 Documentation](https://ldap3.readthedocs.io/)
|
||||||
|
- [LDAP Protocol Overview](https://ldap.com/ldap-protocol/)
|
||||||
|
- [Django LDAP Authentication](https://django-auth-ldap.readthedocs.io/)
|
||||||
|
- [Flask-LDAP3-Login](https://flask-ldap3-login.readthedocs.io/)
|
||||||
|
|
||||||
|
## Contributing Examples
|
||||||
|
|
||||||
|
Have an example for a specific framework or use case? Contributions are welcome!
|
||||||
|
|
||||||
|
Examples we'd love to see:
|
||||||
|
- Express.js / Node.js authentication
|
||||||
|
- Ruby on Rails integration
|
||||||
|
- Go LDAP client
|
||||||
|
- Java Spring Security LDAP
|
||||||
|
- PHP authentication
|
||||||
|
- Docker Compose with application stack
|
||||||
|
|
||||||
|
Submit a pull request with your example!
|
||||||
288
examples/simple_auth.py
Normal file
288
examples/simple_auth.py
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Simple LDAP Authentication Example
|
||||||
|
|
||||||
|
This script demonstrates how to authenticate users against the LDAP server
|
||||||
|
and retrieve user information.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python examples/simple_auth.py
|
||||||
|
python examples/simple_auth.py --username jdoe --password password123
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
from ldap3 import ALL, Connection, Server
|
||||||
|
from ldap3.core.exceptions import LDAPException, LDAPBindError
|
||||||
|
except ImportError:
|
||||||
|
print("Error: ldap3 library not found.")
|
||||||
|
print("Install it with: uv pip install ldap3")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
LDAP_SERVER = "ldap://localhost:389"
|
||||||
|
LDAP_BASE_DN = "dc=testing,dc=local"
|
||||||
|
LDAP_PEOPLE_OU = "ou=people,dc=testing,dc=local"
|
||||||
|
LDAP_GROUPS_OU = "ou=groups,dc=testing,dc=local"
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPAuthenticator:
|
||||||
|
"""Simple LDAP authentication helper."""
|
||||||
|
|
||||||
|
def __init__(self, server_url: str = LDAP_SERVER, base_dn: str = LDAP_BASE_DN):
|
||||||
|
"""Initialize the authenticator."""
|
||||||
|
self.server_url = server_url
|
||||||
|
self.base_dn = base_dn
|
||||||
|
self.server = Server(server_url, get_info=ALL)
|
||||||
|
|
||||||
|
def authenticate(self, username: str, password: str) -> bool:
|
||||||
|
"""
|
||||||
|
Authenticate a user with their username and password.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: The user's uid (e.g., 'jdoe')
|
||||||
|
password: The user's password
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if authentication successful, False otherwise
|
||||||
|
"""
|
||||||
|
# Construct the user's DN
|
||||||
|
user_dn = f"uid={username},{LDAP_PEOPLE_OU}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to bind with the user's credentials
|
||||||
|
conn = Connection(self.server, user=user_dn, password=password)
|
||||||
|
if conn.bind():
|
||||||
|
print(f"✅ Authentication successful for user: {username}")
|
||||||
|
conn.unbind()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"❌ Authentication failed for user: {username}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
except LDAPBindError as e:
|
||||||
|
print(f"❌ Authentication failed: Invalid credentials")
|
||||||
|
return False
|
||||||
|
except LDAPException as e:
|
||||||
|
print(f"❌ LDAP error: {e}")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Unexpected error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_user_info(self, username: str, admin_dn: str, admin_password: str) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Retrieve detailed information about a user.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: The user's uid
|
||||||
|
admin_dn: Admin DN for searching
|
||||||
|
admin_password: Admin password
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with user information or None if not found
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = Connection(self.server, user=admin_dn, password=admin_password, auto_bind=True)
|
||||||
|
|
||||||
|
# Search for the user
|
||||||
|
conn.search(
|
||||||
|
search_base=LDAP_PEOPLE_OU,
|
||||||
|
search_filter=f"(uid={username})",
|
||||||
|
attributes=["uid", "cn", "sn", "givenName", "mail", "uidNumber", "gidNumber"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if conn.entries:
|
||||||
|
entry = conn.entries[0]
|
||||||
|
user_info = {
|
||||||
|
"username": str(entry.uid),
|
||||||
|
"full_name": str(entry.cn),
|
||||||
|
"first_name": str(entry.givenName) if entry.givenName else "",
|
||||||
|
"last_name": str(entry.sn),
|
||||||
|
"email": str(entry.mail),
|
||||||
|
"uid_number": int(entry.uidNumber) if entry.uidNumber else None,
|
||||||
|
"gid_number": int(entry.gidNumber) if entry.gidNumber else None,
|
||||||
|
"dn": entry.entry_dn,
|
||||||
|
}
|
||||||
|
conn.unbind()
|
||||||
|
return user_info
|
||||||
|
else:
|
||||||
|
print(f"User '{username}' not found")
|
||||||
|
conn.unbind()
|
||||||
|
return None
|
||||||
|
|
||||||
|
except LDAPException as e:
|
||||||
|
print(f"Error retrieving user info: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_user_groups(self, username: str, admin_dn: str, admin_password: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
Get all groups that a user belongs to.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username: The user's uid
|
||||||
|
admin_dn: Admin DN for searching
|
||||||
|
admin_password: Admin password
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of group names
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = Connection(self.server, user=admin_dn, password=admin_password, auto_bind=True)
|
||||||
|
|
||||||
|
# Get user's full DN first
|
||||||
|
user_dn = f"uid={username},{LDAP_PEOPLE_OU}"
|
||||||
|
|
||||||
|
# Search for groups that have this user as a member
|
||||||
|
conn.search(
|
||||||
|
search_base=LDAP_GROUPS_OU,
|
||||||
|
search_filter=f"(member={user_dn})",
|
||||||
|
attributes=["cn"],
|
||||||
|
)
|
||||||
|
|
||||||
|
groups = [str(entry.cn) for entry in conn.entries]
|
||||||
|
conn.unbind()
|
||||||
|
return groups
|
||||||
|
|
||||||
|
except LDAPException as e:
|
||||||
|
print(f"Error retrieving user groups: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def list_all_users(self, admin_dn: str, admin_password: str) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
List all users in the directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
admin_dn: Admin DN for searching
|
||||||
|
admin_password: Admin password
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of user dictionaries
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
conn = Connection(self.server, user=admin_dn, password=admin_password, auto_bind=True)
|
||||||
|
|
||||||
|
conn.search(
|
||||||
|
search_base=LDAP_PEOPLE_OU,
|
||||||
|
search_filter="(objectClass=inetOrgPerson)",
|
||||||
|
attributes=["uid", "cn", "mail"],
|
||||||
|
)
|
||||||
|
|
||||||
|
users = []
|
||||||
|
for entry in conn.entries:
|
||||||
|
users.append(
|
||||||
|
{
|
||||||
|
"username": str(entry.uid),
|
||||||
|
"full_name": str(entry.cn),
|
||||||
|
"email": str(entry.mail),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.unbind()
|
||||||
|
return users
|
||||||
|
|
||||||
|
except LDAPException as e:
|
||||||
|
print(f"Error listing users: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def print_user_info(user_info: Dict):
|
||||||
|
"""Pretty print user information."""
|
||||||
|
print("\n" + "=" * 50)
|
||||||
|
print("USER INFORMATION")
|
||||||
|
print("=" * 50)
|
||||||
|
print(f"Username: {user_info['username']}")
|
||||||
|
print(f"Full Name: {user_info['full_name']}")
|
||||||
|
print(f"First Name: {user_info['first_name']}")
|
||||||
|
print(f"Last Name: {user_info['last_name']}")
|
||||||
|
print(f"Email: {user_info['email']}")
|
||||||
|
print(f"UID Number: {user_info['uid_number']}")
|
||||||
|
print(f"GID Number: {user_info['gid_number']}")
|
||||||
|
print(f"DN: {user_info['dn']}")
|
||||||
|
print("=" * 50 + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function demonstrating LDAP authentication."""
|
||||||
|
parser = argparse.ArgumentParser(description="LDAP Authentication Example")
|
||||||
|
parser.add_argument(
|
||||||
|
"--username", "-u", default="jdoe", help="Username to authenticate (default: jdoe)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--password", "-p", default="password123", help="Password (default: password123)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--server", "-s", default=LDAP_SERVER, help=f"LDAP server URL (default: {LDAP_SERVER})"
|
||||||
|
)
|
||||||
|
parser.add_argument("--list-users", action="store_true", help="List all users")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Admin credentials for retrieving user info
|
||||||
|
admin_dn = "cn=admin,dc=testing,dc=local"
|
||||||
|
admin_password = "admin_password"
|
||||||
|
|
||||||
|
print(f"\n🔐 LDAP Authentication Example")
|
||||||
|
print(f"Server: {args.server}\n")
|
||||||
|
|
||||||
|
# Initialize authenticator
|
||||||
|
auth = LDAPAuthenticator(server_url=args.server)
|
||||||
|
|
||||||
|
# List all users if requested
|
||||||
|
if args.list_users:
|
||||||
|
print("📋 Listing all users...")
|
||||||
|
users = auth.list_all_users(admin_dn, admin_password)
|
||||||
|
if users:
|
||||||
|
print(f"\nFound {len(users)} user(s):\n")
|
||||||
|
for user in users:
|
||||||
|
print(f" • {user['username']:12s} - {user['full_name']:20s} ({user['email']})")
|
||||||
|
else:
|
||||||
|
print("No users found")
|
||||||
|
print()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Authenticate user
|
||||||
|
print(f"Attempting to authenticate user: {args.username}")
|
||||||
|
if auth.authenticate(args.username, args.password):
|
||||||
|
print("✅ Authentication successful!\n")
|
||||||
|
|
||||||
|
# Get detailed user information
|
||||||
|
print("Fetching user information...")
|
||||||
|
user_info = auth.get_user_info(args.username, admin_dn, admin_password)
|
||||||
|
if user_info:
|
||||||
|
print_user_info(user_info)
|
||||||
|
|
||||||
|
# Get user's groups
|
||||||
|
print("Fetching user groups...")
|
||||||
|
groups = auth.get_user_groups(args.username, admin_dn, admin_password)
|
||||||
|
if groups:
|
||||||
|
print(f"User belongs to {len(groups)} group(s):")
|
||||||
|
for group in groups:
|
||||||
|
print(f" • {group}")
|
||||||
|
else:
|
||||||
|
print("User is not a member of any groups")
|
||||||
|
print()
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ Authentication failed!")
|
||||||
|
print("\nAvailable test users:")
|
||||||
|
print(" • jdoe (password: password123)")
|
||||||
|
print(" • jsmith (password: password123)")
|
||||||
|
print(" • testuser (password: password123)")
|
||||||
|
print(" • admin (password: password123)")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n\nInterrupted by user")
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\nError: {e}")
|
||||||
|
sys.exit(1)
|
||||||
109
ldif/01-users.ldif
Normal file
109
ldif/01-users.ldif
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# Sample LDIF file for testing.local domain
|
||||||
|
# This file creates organizational units, users, and groups for testing
|
||||||
|
|
||||||
|
# Create organizational units
|
||||||
|
dn: ou=people,dc=testing,dc=local
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
objectClass: top
|
||||||
|
ou: people
|
||||||
|
description: Users in the testing organization
|
||||||
|
|
||||||
|
dn: ou=groups,dc=testing,dc=local
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
objectClass: top
|
||||||
|
ou: groups
|
||||||
|
description: Groups in the testing organization
|
||||||
|
|
||||||
|
# Create test users
|
||||||
|
dn: uid=jdoe,ou=people,dc=testing,dc=local
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: posixAccount
|
||||||
|
objectClass: shadowAccount
|
||||||
|
objectClass: top
|
||||||
|
uid: jdoe
|
||||||
|
cn: John Doe
|
||||||
|
sn: Doe
|
||||||
|
givenName: John
|
||||||
|
mail: jdoe@testing.local
|
||||||
|
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ= # password: password123
|
||||||
|
uidNumber: 10001
|
||||||
|
gidNumber: 10001
|
||||||
|
homeDirectory: /home/jdoe
|
||||||
|
loginShell: /bin/bash
|
||||||
|
description: Test user - John Doe
|
||||||
|
|
||||||
|
dn: uid=jsmith,ou=people,dc=testing,dc=local
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: posixAccount
|
||||||
|
objectClass: shadowAccount
|
||||||
|
objectClass: top
|
||||||
|
uid: jsmith
|
||||||
|
cn: Jane Smith
|
||||||
|
sn: Smith
|
||||||
|
givenName: Jane
|
||||||
|
mail: jsmith@testing.local
|
||||||
|
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ= # password: password123
|
||||||
|
uidNumber: 10002
|
||||||
|
gidNumber: 10002
|
||||||
|
homeDirectory: /home/jsmith
|
||||||
|
loginShell: /bin/bash
|
||||||
|
description: Test user - Jane Smith
|
||||||
|
|
||||||
|
dn: uid=admin,ou=people,dc=testing,dc=local
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: posixAccount
|
||||||
|
objectClass: shadowAccount
|
||||||
|
objectClass: top
|
||||||
|
uid: admin
|
||||||
|
cn: Admin User
|
||||||
|
sn: User
|
||||||
|
givenName: Admin
|
||||||
|
mail: admin@testing.local
|
||||||
|
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ= # password: password123
|
||||||
|
uidNumber: 10000
|
||||||
|
gidNumber: 10000
|
||||||
|
homeDirectory: /home/admin
|
||||||
|
loginShell: /bin/bash
|
||||||
|
description: Test administrator user
|
||||||
|
|
||||||
|
dn: uid=testuser,ou=people,dc=testing,dc=local
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
objectClass: posixAccount
|
||||||
|
objectClass: shadowAccount
|
||||||
|
objectClass: top
|
||||||
|
uid: testuser
|
||||||
|
cn: Test User
|
||||||
|
sn: User
|
||||||
|
givenName: Test
|
||||||
|
mail: testuser@testing.local
|
||||||
|
userPassword: {SSHA}5en6G6MezRroT3XKqkdPOmY/BFQ= # password: password123
|
||||||
|
uidNumber: 10003
|
||||||
|
gidNumber: 10003
|
||||||
|
homeDirectory: /home/testuser
|
||||||
|
loginShell: /bin/bash
|
||||||
|
description: Generic test user
|
||||||
|
|
||||||
|
# Create test groups
|
||||||
|
dn: cn=developers,ou=groups,dc=testing,dc=local
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
||||||
|
cn: developers
|
||||||
|
description: Development team group
|
||||||
|
member: uid=jdoe,ou=people,dc=testing,dc=local
|
||||||
|
member: uid=jsmith,ou=people,dc=testing,dc=local
|
||||||
|
|
||||||
|
dn: cn=admins,ou=groups,dc=testing,dc=local
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
||||||
|
cn: admins
|
||||||
|
description: Administrator group
|
||||||
|
member: uid=admin,ou=people,dc=testing,dc=local
|
||||||
|
|
||||||
|
dn: cn=users,ou=groups,dc=testing,dc=local
|
||||||
|
objectClass: groupOfNames
|
||||||
|
objectClass: top
|
||||||
|
cn: users
|
||||||
|
description: General users group
|
||||||
|
member: uid=jdoe,ou=people,dc=testing,dc=local
|
||||||
|
member: uid=jsmith,ou=people,dc=testing,dc=local
|
||||||
|
member: uid=testuser,ou=people,dc=testing,dc=local
|
||||||
87
pyproject.toml
Normal file
87
pyproject.toml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
[project]
|
||||||
|
name = "ldap-docker"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A development tool for running OpenLDAP with SSL in Docker"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
authors = [
|
||||||
|
{ name = "Spencer" }
|
||||||
|
]
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
"ldap3>=2.9.1",
|
||||||
|
"cryptography>=41.0.0",
|
||||||
|
"click>=8.1.0",
|
||||||
|
"python-dotenv>=1.0.0",
|
||||||
|
"pyyaml>=6.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"pytest>=7.4.0",
|
||||||
|
"pytest-cov>=4.1.0",
|
||||||
|
"black>=23.0.0",
|
||||||
|
"ruff>=0.1.0",
|
||||||
|
"mypy>=1.5.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
ldap-docker = "scripts.cli:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
dev-dependencies = [
|
||||||
|
"pytest>=7.4.0",
|
||||||
|
"pytest-cov>=4.1.0",
|
||||||
|
"black>=23.0.0",
|
||||||
|
"ruff>=0.1.0",
|
||||||
|
"mypy>=1.5.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 100
|
||||||
|
target-version = ["py39", "py310", "py311", "py312"]
|
||||||
|
include = '\.pyi?$'
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
line-length = 100
|
||||||
|
target-version = "py39"
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"UP", # pyupgrade
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E501", # line too long (handled by black)
|
||||||
|
"B008", # do not perform function calls in argument defaults
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.per-file-ignores]
|
||||||
|
"__init__.py" = ["F401"]
|
||||||
|
|
||||||
|
[tool.mypy]
|
||||||
|
python_version = "3.9"
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
disallow_untyped_defs = false
|
||||||
|
ignore_missing_imports = true
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = [
|
||||||
|
"--verbose",
|
||||||
|
"--cov=scripts",
|
||||||
|
"--cov-report=term-missing",
|
||||||
|
"--cov-report=html",
|
||||||
|
]
|
||||||
381
quickstart.sh
Executable file
381
quickstart.sh
Executable file
@@ -0,0 +1,381 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Quick Start Script for LDAP Docker
|
||||||
|
# This script will guide you through setting up and starting the LDAP server
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Emoji support (works on most terminals)
|
||||||
|
CHECK_MARK="✅"
|
||||||
|
CROSS_MARK="❌"
|
||||||
|
WARNING="⚠️"
|
||||||
|
ROCKET="🚀"
|
||||||
|
GEAR="⚙️"
|
||||||
|
|
||||||
|
# Get the directory where this script is located
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Functions
|
||||||
|
print_header() {
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
echo -e "${BLUE}$1${NC}"
|
||||||
|
echo -e "${BLUE}================================================${NC}"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}${CHECK_MARK} $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}${CROSS_MARK} $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}${WARNING} $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}${GEAR} $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if command exists
|
||||||
|
command_exists() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check Docker
|
||||||
|
check_docker() {
|
||||||
|
print_info "Checking Docker..."
|
||||||
|
|
||||||
|
if command_exists docker; then
|
||||||
|
if docker version >/dev/null 2>&1; then
|
||||||
|
print_success "Docker is installed and running"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Docker is installed but not running"
|
||||||
|
echo ""
|
||||||
|
echo "Please start Docker or Rancher Desktop and try again."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Docker is not installed"
|
||||||
|
echo ""
|
||||||
|
echo "Please install one of the following:"
|
||||||
|
echo " - Rancher Desktop (recommended for MacOS): https://rancherdesktop.io/"
|
||||||
|
echo " - Docker Desktop: https://www.docker.com/products/docker-desktop"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check docker-compose
|
||||||
|
check_docker_compose() {
|
||||||
|
print_info "Checking docker-compose..."
|
||||||
|
|
||||||
|
if command_exists docker-compose; then
|
||||||
|
print_success "docker-compose is available"
|
||||||
|
return 0
|
||||||
|
elif docker compose version >/dev/null 2>&1; then
|
||||||
|
print_success "docker compose (v2) is available"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_warning "docker-compose not found"
|
||||||
|
echo "Docker Compose is usually included with Docker Desktop and Rancher Desktop."
|
||||||
|
echo "If you're using a standalone Docker installation, please install docker-compose."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check Python
|
||||||
|
check_python() {
|
||||||
|
print_info "Checking Python..."
|
||||||
|
|
||||||
|
if command_exists python3; then
|
||||||
|
PYTHON_VERSION=$(python3 --version | cut -d' ' -f2)
|
||||||
|
print_success "Python ${PYTHON_VERSION} is installed"
|
||||||
|
return 0
|
||||||
|
elif command_exists python; then
|
||||||
|
PYTHON_VERSION=$(python --version | cut -d' ' -f2)
|
||||||
|
print_success "Python ${PYTHON_VERSION} is installed"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Python is not installed"
|
||||||
|
echo "Please install Python 3.9 or higher."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check/Install UV
|
||||||
|
check_install_uv() {
|
||||||
|
print_info "Checking UV package manager..."
|
||||||
|
|
||||||
|
if command_exists uv; then
|
||||||
|
UV_VERSION=$(uv --version | cut -d' ' -f2 || echo "unknown")
|
||||||
|
print_success "UV ${UV_VERSION} is installed"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_warning "UV is not installed"
|
||||||
|
echo ""
|
||||||
|
echo "UV is a fast Python package manager (recommended for this project)."
|
||||||
|
read -p "Would you like to install UV now? [Y/n] " -n 1 -r
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
|
||||||
|
print_info "Installing UV..."
|
||||||
|
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||||
|
|
||||||
|
# Source the shell config to get UV in PATH
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
|
||||||
|
if command_exists uv; then
|
||||||
|
print_success "UV installed successfully"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "UV installation may have completed but it's not in PATH"
|
||||||
|
echo "Please restart your terminal and run this script again."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "Skipping UV installation"
|
||||||
|
echo "You can install dependencies manually with pip if needed."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check certificates
|
||||||
|
check_certificates() {
|
||||||
|
print_info "Checking SSL certificates..."
|
||||||
|
|
||||||
|
if [[ -f "certs/ca.crt" && -f "certs/server.crt" && -f "certs/server.key" ]]; then
|
||||||
|
print_success "SSL certificates found"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_warning "SSL certificates not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate certificates
|
||||||
|
generate_certificates() {
|
||||||
|
print_info "Generating SSL certificates..."
|
||||||
|
|
||||||
|
if command_exists uv && [[ -f "scripts/generate_certs.py" ]]; then
|
||||||
|
uv run python scripts/generate_certs.py
|
||||||
|
print_success "Certificates generated"
|
||||||
|
elif command_exists python3; then
|
||||||
|
python3 scripts/generate_certs.py
|
||||||
|
print_success "Certificates generated"
|
||||||
|
else
|
||||||
|
print_error "Cannot generate certificates - Python not available"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
install_dependencies() {
|
||||||
|
print_info "Installing Python dependencies..."
|
||||||
|
|
||||||
|
if command_exists uv; then
|
||||||
|
uv sync
|
||||||
|
print_success "Dependencies installed with UV"
|
||||||
|
elif command_exists python3 && command_exists pip3; then
|
||||||
|
pip3 install -e .
|
||||||
|
print_success "Dependencies installed with pip"
|
||||||
|
else
|
||||||
|
print_warning "Cannot install dependencies automatically"
|
||||||
|
echo "Please install dependencies manually."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start LDAP server
|
||||||
|
start_server() {
|
||||||
|
print_info "Starting LDAP server..."
|
||||||
|
|
||||||
|
if command_exists docker-compose; then
|
||||||
|
docker-compose up -d
|
||||||
|
else
|
||||||
|
docker compose up -d
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_success "LDAP server started"
|
||||||
|
|
||||||
|
# Wait a bit for the server to initialize
|
||||||
|
print_info "Waiting for server to initialize (10 seconds)..."
|
||||||
|
sleep 10
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test connection
|
||||||
|
test_connection() {
|
||||||
|
print_info "Testing LDAP connection..."
|
||||||
|
|
||||||
|
# Simple check if container is running
|
||||||
|
if docker ps | grep -q "ldap-server"; then
|
||||||
|
print_success "LDAP server container is running"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_warning "LDAP server container may not be fully started yet"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print final information
|
||||||
|
print_final_info() {
|
||||||
|
echo ""
|
||||||
|
print_header "${ROCKET} LDAP Server is Ready! ${ROCKET}"
|
||||||
|
|
||||||
|
echo "Services are now available at:"
|
||||||
|
echo ""
|
||||||
|
echo " ${GREEN}LDAP:${NC} ldap://localhost:389"
|
||||||
|
echo " ${GREEN}LDAPS:${NC} ldaps://localhost:636"
|
||||||
|
echo " ${GREEN}Admin UI:${NC} http://localhost:8080"
|
||||||
|
echo ""
|
||||||
|
echo "Admin Credentials:"
|
||||||
|
echo " ${BLUE}DN:${NC} cn=admin,dc=testing,dc=local"
|
||||||
|
echo " ${BLUE}Password:${NC} admin_password"
|
||||||
|
echo ""
|
||||||
|
echo "Test Users (password: password123):"
|
||||||
|
echo " - jdoe (John Doe)"
|
||||||
|
echo " - jsmith (Jane Smith)"
|
||||||
|
echo " - testuser (Test User)"
|
||||||
|
echo ""
|
||||||
|
echo "Useful Commands:"
|
||||||
|
echo " ${BLUE}make logs${NC} - View server logs"
|
||||||
|
echo " ${BLUE}make test-users${NC} - List all users"
|
||||||
|
echo " ${BLUE}make status${NC} - Check server status"
|
||||||
|
echo " ${BLUE}make stop${NC} - Stop the server"
|
||||||
|
echo " ${BLUE}make help${NC} - See all available commands"
|
||||||
|
echo ""
|
||||||
|
echo "Documentation:"
|
||||||
|
echo " - README.md for full documentation"
|
||||||
|
echo " - certs/README.md for certificate management"
|
||||||
|
echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main script
|
||||||
|
main() {
|
||||||
|
clear
|
||||||
|
print_header "${ROCKET} LDAP Docker Quick Start ${ROCKET}"
|
||||||
|
|
||||||
|
echo "This script will set up and start your LDAP development server."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 1: Check prerequisites
|
||||||
|
print_header "Step 1: Checking Prerequisites"
|
||||||
|
|
||||||
|
PREREQ_OK=true
|
||||||
|
|
||||||
|
check_docker || PREREQ_OK=false
|
||||||
|
check_docker_compose || PREREQ_OK=false
|
||||||
|
check_python || PREREQ_OK=false
|
||||||
|
|
||||||
|
if [ "$PREREQ_OK" = false ]; then
|
||||||
|
echo ""
|
||||||
|
print_error "Some prerequisites are missing. Please install them and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
print_success "All prerequisites are met!"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 2: UV installation (optional but recommended)
|
||||||
|
print_header "Step 2: Package Manager Setup"
|
||||||
|
check_install_uv
|
||||||
|
HAS_UV=$?
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 3: SSL Certificates
|
||||||
|
print_header "Step 3: SSL Certificate Setup"
|
||||||
|
|
||||||
|
if check_certificates; then
|
||||||
|
echo ""
|
||||||
|
print_info "Existing certificates will be used."
|
||||||
|
read -p "Regenerate certificates? [y/N] " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
generate_certificates
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
echo "SSL certificates are required for LDAPS (secure LDAP)."
|
||||||
|
echo ""
|
||||||
|
echo "You can:"
|
||||||
|
echo " 1. Generate self-signed certificates now (recommended for quick start)"
|
||||||
|
echo " 2. Copy certificates from your dev-ca manually to certs/"
|
||||||
|
echo ""
|
||||||
|
read -p "Generate self-signed certificates now? [Y/n] " -n 1 -r
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]] || [[ -z $REPLY ]]; then
|
||||||
|
generate_certificates
|
||||||
|
else
|
||||||
|
print_info "Please copy your certificates to certs/ directory:"
|
||||||
|
echo " - certs/ca.crt"
|
||||||
|
echo " - certs/server.crt"
|
||||||
|
echo " - certs/server.key"
|
||||||
|
echo ""
|
||||||
|
read -p "Press Enter when ready to continue..."
|
||||||
|
|
||||||
|
if ! check_certificates; then
|
||||||
|
print_error "Certificates still not found. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 4: Install dependencies (optional)
|
||||||
|
if [ $HAS_UV -eq 0 ]; then
|
||||||
|
print_header "Step 4: Installing Dependencies"
|
||||||
|
install_dependencies
|
||||||
|
echo ""
|
||||||
|
else
|
||||||
|
print_info "Skipping dependency installation (UV not available)"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 5: Start server
|
||||||
|
print_header "Step 5: Starting LDAP Server"
|
||||||
|
start_server
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Step 6: Test
|
||||||
|
print_header "Step 6: Verifying Installation"
|
||||||
|
test_connection
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Success!
|
||||||
|
print_final_info
|
||||||
|
|
||||||
|
# Offer to view logs
|
||||||
|
echo ""
|
||||||
|
read -p "View server logs now? [y/N] " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
echo ""
|
||||||
|
print_info "Showing logs (press Ctrl+C to exit)..."
|
||||||
|
sleep 2
|
||||||
|
if command_exists docker-compose; then
|
||||||
|
docker-compose logs -f openldap
|
||||||
|
else
|
||||||
|
docker compose logs -f openldap
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run main function
|
||||||
|
main
|
||||||
|
|
||||||
|
exit 0
|
||||||
13
scripts/__init__.py
Normal file
13
scripts/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""
|
||||||
|
LDAP Docker Management Scripts
|
||||||
|
|
||||||
|
This package contains management and utility scripts for the LDAP Docker
|
||||||
|
development environment.
|
||||||
|
|
||||||
|
Modules:
|
||||||
|
- cli: Command-line interface for managing LDAP server
|
||||||
|
- generate_certs: SSL/TLS certificate generation utility
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
__all__ = ["cli", "generate_certs"]
|
||||||
463
scripts/cli.py
Normal file
463
scripts/cli.py
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
CLI tool for managing the LDAP Docker development environment.
|
||||||
|
|
||||||
|
This tool provides convenient commands for starting, stopping, and testing
|
||||||
|
the LDAP server, as well as managing test users and certificates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import click
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ldap3
|
||||||
|
from ldap3 import ALL, Connection, Server
|
||||||
|
from ldap3.core.exceptions import LDAPException
|
||||||
|
except ImportError:
|
||||||
|
ldap3 = None
|
||||||
|
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
CERTS_DIR = PROJECT_ROOT / "certs"
|
||||||
|
LDIF_DIR = PROJECT_ROOT / "ldif"
|
||||||
|
DEFAULT_HOST = "localhost"
|
||||||
|
DEFAULT_PORT = 389
|
||||||
|
DEFAULT_LDAPS_PORT = 636
|
||||||
|
DEFAULT_BASE_DN = "dc=testing,dc=local"
|
||||||
|
DEFAULT_ADMIN_DN = "cn=admin,dc=testing,dc=local"
|
||||||
|
DEFAULT_ADMIN_PASSWORD = "admin_password"
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(cmd: list[str], cwd: Optional[Path] = None, check: bool = True) -> subprocess.CompletedProcess:
|
||||||
|
"""Run a shell command and return the result."""
|
||||||
|
if cwd is None:
|
||||||
|
cwd = PROJECT_ROOT
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
cwd=cwd,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=check,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
click.echo(f"Error running command: {' '.join(cmd)}", err=True)
|
||||||
|
click.echo(f"Exit code: {e.returncode}", err=True)
|
||||||
|
if e.stdout:
|
||||||
|
click.echo(f"stdout: {e.stdout}", err=True)
|
||||||
|
if e.stderr:
|
||||||
|
click.echo(f"stderr: {e.stderr}", err=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def check_docker():
|
||||||
|
"""Check if Docker is available."""
|
||||||
|
try:
|
||||||
|
result = run_command(["docker", "version"], check=False)
|
||||||
|
if result.returncode != 0:
|
||||||
|
click.echo("Error: Docker is not running or not installed.", err=True)
|
||||||
|
click.echo("Please ensure Docker (or Rancher Desktop) is running.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
except FileNotFoundError:
|
||||||
|
click.echo("Error: Docker command not found.", err=True)
|
||||||
|
click.echo("Please install Docker or Rancher Desktop.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def check_certificates():
|
||||||
|
"""Check if SSL certificates exist."""
|
||||||
|
required_files = ["ca.crt", "server.crt", "server.key"]
|
||||||
|
missing = [f for f in required_files if not (CERTS_DIR / f).exists()]
|
||||||
|
|
||||||
|
if missing:
|
||||||
|
click.echo("⚠️ Warning: Missing SSL certificate files:", err=True)
|
||||||
|
for f in missing:
|
||||||
|
click.echo(f" - {CERTS_DIR / f}", err=True)
|
||||||
|
click.echo("\nYou can:", err=True)
|
||||||
|
click.echo(" 1. Copy your dev-ca certificates to the certs/ directory", err=True)
|
||||||
|
click.echo(" 2. Generate self-signed certificates: ldap-docker certs generate", err=True)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@click.group()
|
||||||
|
@click.version_option(version="0.1.0")
|
||||||
|
def cli():
|
||||||
|
"""LDAP Docker Development Tool - Manage your OpenLDAP development environment."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@cli.group()
|
||||||
|
def server():
|
||||||
|
"""Manage the LDAP server container."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@server.command("start")
|
||||||
|
@click.option("--detach", "-d", is_flag=True, default=True, help="Run in detached mode")
|
||||||
|
@click.option("--build", is_flag=True, help="Build images before starting")
|
||||||
|
def server_start(detach: bool, build: bool):
|
||||||
|
"""Start the LDAP server and optional phpLDAPadmin."""
|
||||||
|
check_docker()
|
||||||
|
check_certificates()
|
||||||
|
|
||||||
|
cmd = ["docker-compose", "up"]
|
||||||
|
if detach:
|
||||||
|
cmd.append("-d")
|
||||||
|
if build:
|
||||||
|
cmd.append("--build")
|
||||||
|
|
||||||
|
click.echo("Starting LDAP server...")
|
||||||
|
result = run_command(cmd)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
click.echo("✅ LDAP server started successfully!")
|
||||||
|
click.echo(f"\nLDAP server is available at:")
|
||||||
|
click.echo(f" - LDAP: ldap://localhost:389")
|
||||||
|
click.echo(f" - LDAPS: ldaps://localhost:636")
|
||||||
|
click.echo(f" - Admin: http://localhost:8080 (phpLDAPadmin)")
|
||||||
|
click.echo(f"\nAdmin credentials:")
|
||||||
|
click.echo(f" - DN: {DEFAULT_ADMIN_DN}")
|
||||||
|
click.echo(f" - Password: {DEFAULT_ADMIN_PASSWORD}")
|
||||||
|
|
||||||
|
if detach:
|
||||||
|
click.echo("\nWaiting for server to be ready...")
|
||||||
|
time.sleep(5)
|
||||||
|
# Try to check if server is responding
|
||||||
|
result = run_command(
|
||||||
|
["docker-compose", "ps", "--filter", "status=running"],
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
if "ldap-server" in result.stdout:
|
||||||
|
click.echo("✅ Server is running")
|
||||||
|
|
||||||
|
|
||||||
|
@server.command("stop")
|
||||||
|
def server_stop():
|
||||||
|
"""Stop the LDAP server."""
|
||||||
|
check_docker()
|
||||||
|
|
||||||
|
click.echo("Stopping LDAP server...")
|
||||||
|
run_command(["docker-compose", "stop"])
|
||||||
|
click.echo("✅ LDAP server stopped")
|
||||||
|
|
||||||
|
|
||||||
|
@server.command("restart")
|
||||||
|
def server_restart():
|
||||||
|
"""Restart the LDAP server."""
|
||||||
|
check_docker()
|
||||||
|
|
||||||
|
click.echo("Restarting LDAP server...")
|
||||||
|
run_command(["docker-compose", "restart"])
|
||||||
|
click.echo("✅ LDAP server restarted")
|
||||||
|
|
||||||
|
click.echo("\nWaiting for server to be ready...")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
|
@server.command("down")
|
||||||
|
@click.option("--volumes", "-v", is_flag=True, help="Remove volumes (deletes all data)")
|
||||||
|
def server_down(volumes: bool):
|
||||||
|
"""Stop and remove the LDAP server containers."""
|
||||||
|
check_docker()
|
||||||
|
|
||||||
|
cmd = ["docker-compose", "down"]
|
||||||
|
if volumes:
|
||||||
|
if not click.confirm("⚠️ This will delete all LDAP data. Continue?"):
|
||||||
|
click.echo("Aborted.")
|
||||||
|
return
|
||||||
|
cmd.append("-v")
|
||||||
|
|
||||||
|
click.echo("Removing LDAP server containers...")
|
||||||
|
run_command(cmd)
|
||||||
|
click.echo("✅ Containers removed")
|
||||||
|
|
||||||
|
|
||||||
|
@server.command("logs")
|
||||||
|
@click.option("--follow", "-f", is_flag=True, help="Follow log output")
|
||||||
|
@click.option("--tail", "-n", default=100, help="Number of lines to show from the end")
|
||||||
|
@click.option("--service", default="openldap", help="Service to show logs for")
|
||||||
|
def server_logs(follow: bool, tail: int, service: str):
|
||||||
|
"""View LDAP server logs."""
|
||||||
|
check_docker()
|
||||||
|
|
||||||
|
cmd = ["docker-compose", "logs", f"--tail={tail}"]
|
||||||
|
if follow:
|
||||||
|
cmd.append("-f")
|
||||||
|
cmd.append(service)
|
||||||
|
|
||||||
|
# For follow mode, we want to pass through to the terminal
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, cwd=PROJECT_ROOT)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
click.echo("\n")
|
||||||
|
|
||||||
|
|
||||||
|
@server.command("status")
|
||||||
|
def server_status():
|
||||||
|
"""Check LDAP server status."""
|
||||||
|
check_docker()
|
||||||
|
|
||||||
|
result = run_command(["docker-compose", "ps"], check=False)
|
||||||
|
click.echo(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.group()
|
||||||
|
def certs():
|
||||||
|
"""Manage SSL/TLS certificates."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@certs.command("generate")
|
||||||
|
@click.option("--force", is_flag=True, help="Overwrite existing certificates")
|
||||||
|
@click.option("--hostname", default="ldap.testing.local", help="Server hostname")
|
||||||
|
def certs_generate(force: bool, hostname: str):
|
||||||
|
"""Generate self-signed SSL certificates for development."""
|
||||||
|
script_path = PROJECT_ROOT / "scripts" / "generate_certs.py"
|
||||||
|
|
||||||
|
if not script_path.exists():
|
||||||
|
click.echo(f"Error: Certificate generation script not found: {script_path}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
cmd = [sys.executable, str(script_path), "--hostname", hostname]
|
||||||
|
if force:
|
||||||
|
cmd.append("--force")
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
click.echo("Failed to generate certificates", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@certs.command("check")
|
||||||
|
def certs_check():
|
||||||
|
"""Verify SSL certificates."""
|
||||||
|
required_files = {
|
||||||
|
"ca.crt": "CA Certificate",
|
||||||
|
"server.crt": "Server Certificate",
|
||||||
|
"server.key": "Server Private Key",
|
||||||
|
}
|
||||||
|
|
||||||
|
click.echo("Checking SSL certificates...\n")
|
||||||
|
|
||||||
|
all_exist = True
|
||||||
|
for filename, description in required_files.items():
|
||||||
|
filepath = CERTS_DIR / filename
|
||||||
|
if filepath.exists():
|
||||||
|
size = filepath.stat().st_size
|
||||||
|
click.echo(f"✅ {description}: {filepath} ({size} bytes)")
|
||||||
|
else:
|
||||||
|
click.echo(f"❌ {description}: {filepath} (missing)")
|
||||||
|
all_exist = False
|
||||||
|
|
||||||
|
if all_exist:
|
||||||
|
click.echo("\n✅ All required certificates are present")
|
||||||
|
|
||||||
|
# Try to verify the certificate chain
|
||||||
|
try:
|
||||||
|
result = run_command([
|
||||||
|
"openssl", "verify", "-CAfile",
|
||||||
|
str(CERTS_DIR / "ca.crt"),
|
||||||
|
str(CERTS_DIR / "server.crt")
|
||||||
|
], check=False)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
click.echo("✅ Certificate chain is valid")
|
||||||
|
else:
|
||||||
|
click.echo("⚠️ Certificate chain verification failed")
|
||||||
|
click.echo(result.stderr)
|
||||||
|
except FileNotFoundError:
|
||||||
|
click.echo("ℹ️ OpenSSL not found, skipping certificate verification")
|
||||||
|
else:
|
||||||
|
click.echo("\n❌ Some certificates are missing")
|
||||||
|
click.echo("Run 'ldap-docker certs generate' to create them")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.group()
|
||||||
|
def test():
|
||||||
|
"""Test LDAP server connectivity and queries."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@test.command("connection")
|
||||||
|
@click.option("--host", default=DEFAULT_HOST, help="LDAP server host")
|
||||||
|
@click.option("--port", default=DEFAULT_PORT, help="LDAP server port")
|
||||||
|
@click.option("--use-ssl", is_flag=True, help="Use LDAPS instead of LDAP")
|
||||||
|
def test_connection(host: str, port: int, use_ssl: bool):
|
||||||
|
"""Test basic connection to LDAP server."""
|
||||||
|
if ldap3 is None:
|
||||||
|
click.echo("Error: ldap3 library not installed", err=True)
|
||||||
|
click.echo("Install it with: uv pip install ldap3", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if use_ssl:
|
||||||
|
port = DEFAULT_LDAPS_PORT
|
||||||
|
url = f"ldaps://{host}:{port}"
|
||||||
|
else:
|
||||||
|
url = f"ldap://{host}:{port}"
|
||||||
|
|
||||||
|
click.echo(f"Testing connection to {url}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
server = Server(url, get_info=ALL, use_ssl=use_ssl)
|
||||||
|
conn = Connection(server, auto_bind=True)
|
||||||
|
|
||||||
|
click.echo("✅ Successfully connected to LDAP server")
|
||||||
|
click.echo(f"\nServer info:")
|
||||||
|
click.echo(f" Vendor: {server.info.vendor_name if server.info else 'Unknown'}")
|
||||||
|
click.echo(f" Version: {server.info.vendor_version if server.info else 'Unknown'}")
|
||||||
|
|
||||||
|
conn.unbind()
|
||||||
|
|
||||||
|
except LDAPException as e:
|
||||||
|
click.echo(f"❌ Connection failed: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@test.command("auth")
|
||||||
|
@click.option("--host", default=DEFAULT_HOST, help="LDAP server host")
|
||||||
|
@click.option("--port", default=DEFAULT_PORT, help="LDAP server port")
|
||||||
|
@click.option("--use-ssl", is_flag=True, help="Use LDAPS")
|
||||||
|
@click.option("--user", default=DEFAULT_ADMIN_DN, help="User DN")
|
||||||
|
@click.option("--password", default=DEFAULT_ADMIN_PASSWORD, help="Password")
|
||||||
|
def test_auth(host: str, port: int, use_ssl: bool, user: str, password: str):
|
||||||
|
"""Test authentication with LDAP server."""
|
||||||
|
if ldap3 is None:
|
||||||
|
click.echo("Error: ldap3 library not installed", err=True)
|
||||||
|
click.echo("Install it with: uv pip install ldap3", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if use_ssl:
|
||||||
|
port = DEFAULT_LDAPS_PORT
|
||||||
|
url = f"ldaps://{host}:{port}"
|
||||||
|
else:
|
||||||
|
url = f"ldap://{host}:{port}"
|
||||||
|
|
||||||
|
click.echo(f"Testing authentication to {url}...")
|
||||||
|
click.echo(f"User: {user}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
server = Server(url, get_info=ALL, use_ssl=use_ssl)
|
||||||
|
conn = Connection(server, user=user, password=password, auto_bind=True)
|
||||||
|
|
||||||
|
click.echo("✅ Authentication successful")
|
||||||
|
|
||||||
|
# Try to perform a simple search
|
||||||
|
conn.search(DEFAULT_BASE_DN, "(objectClass=*)", search_scope="BASE")
|
||||||
|
if conn.entries:
|
||||||
|
click.echo(f"✅ Base DN accessible: {DEFAULT_BASE_DN}")
|
||||||
|
|
||||||
|
conn.unbind()
|
||||||
|
|
||||||
|
except LDAPException as e:
|
||||||
|
click.echo(f"❌ Authentication failed: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@test.command("users")
|
||||||
|
@click.option("--host", default=DEFAULT_HOST, help="LDAP server host")
|
||||||
|
@click.option("--port", default=DEFAULT_PORT, help="LDAP server port")
|
||||||
|
@click.option("--use-ssl", is_flag=True, help="Use LDAPS")
|
||||||
|
def test_users(host: str, port: int, use_ssl: bool):
|
||||||
|
"""List all users in the LDAP directory."""
|
||||||
|
if ldap3 is None:
|
||||||
|
click.echo("Error: ldap3 library not installed", err=True)
|
||||||
|
click.echo("Install it with: uv pip install ldap3", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if use_ssl:
|
||||||
|
port = DEFAULT_LDAPS_PORT
|
||||||
|
url = f"ldaps://{host}:{port}"
|
||||||
|
else:
|
||||||
|
url = f"ldap://{host}:{port}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
server = Server(url, get_info=ALL, use_ssl=use_ssl)
|
||||||
|
conn = Connection(
|
||||||
|
server,
|
||||||
|
user=DEFAULT_ADMIN_DN,
|
||||||
|
password=DEFAULT_ADMIN_PASSWORD,
|
||||||
|
auto_bind=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Search for all users
|
||||||
|
conn.search(
|
||||||
|
DEFAULT_BASE_DN,
|
||||||
|
"(objectClass=inetOrgPerson)",
|
||||||
|
attributes=["uid", "cn", "mail", "uidNumber"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if conn.entries:
|
||||||
|
click.echo(f"Found {len(conn.entries)} user(s):\n")
|
||||||
|
for entry in conn.entries:
|
||||||
|
click.echo(f" - {entry.cn}: {entry.uid} ({entry.mail})")
|
||||||
|
else:
|
||||||
|
click.echo("No users found")
|
||||||
|
|
||||||
|
conn.unbind()
|
||||||
|
|
||||||
|
except LDAPException as e:
|
||||||
|
click.echo(f"❌ Query failed: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command("init")
|
||||||
|
def init():
|
||||||
|
"""Initialize the LDAP Docker environment."""
|
||||||
|
click.echo("Initializing LDAP Docker environment...\n")
|
||||||
|
|
||||||
|
# Check Docker
|
||||||
|
click.echo("1. Checking Docker...")
|
||||||
|
check_docker()
|
||||||
|
click.echo(" ✅ Docker is available\n")
|
||||||
|
|
||||||
|
# Check certificates
|
||||||
|
click.echo("2. Checking SSL certificates...")
|
||||||
|
if not check_certificates():
|
||||||
|
if click.confirm("\nGenerate self-signed certificates now?", default=True):
|
||||||
|
certs_generate.callback(force=False, hostname="ldap.testing.local")
|
||||||
|
else:
|
||||||
|
click.echo("\nℹ️ You can generate certificates later with: ldap-docker certs generate")
|
||||||
|
click.echo(" Or copy your dev-ca certificates to the certs/ directory")
|
||||||
|
else:
|
||||||
|
click.echo(" ✅ Certificates are present\n")
|
||||||
|
|
||||||
|
# Start server
|
||||||
|
click.echo("\n3. Starting LDAP server...")
|
||||||
|
if click.confirm("Start the LDAP server now?", default=True):
|
||||||
|
server_start.callback(detach=True, build=False)
|
||||||
|
else:
|
||||||
|
click.echo("\nℹ️ You can start the server later with: ldap-docker server start")
|
||||||
|
|
||||||
|
click.echo("\n✅ Initialization complete!")
|
||||||
|
click.echo("\nUseful commands:")
|
||||||
|
click.echo(" - View logs: ldap-docker server logs -f")
|
||||||
|
click.echo(" - Test connection: ldap-docker test connection")
|
||||||
|
click.echo(" - List users: ldap-docker test users")
|
||||||
|
click.echo(" - Stop server: ldap-docker server stop")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Entry point for the CLI."""
|
||||||
|
try:
|
||||||
|
cli()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
click.echo("\n\nInterrupted by user")
|
||||||
|
sys.exit(130)
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"Error: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
299
scripts/generate_certs.py
Normal file
299
scripts/generate_certs.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate self-signed SSL/TLS certificates for LDAP server testing.
|
||||||
|
|
||||||
|
This script creates a CA certificate and a server certificate for development use.
|
||||||
|
For production, use proper certificates from your dev-ca or a trusted CA.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ipaddress
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.x509.oid import ExtensionOID, NameOID
|
||||||
|
except ImportError:
|
||||||
|
print("Error: cryptography library not found.")
|
||||||
|
print("Install it with: uv pip install cryptography")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_private_key(key_size: int = 4096) -> rsa.RSAPrivateKey:
|
||||||
|
"""Generate an RSA private key."""
|
||||||
|
return rsa.generate_private_key(
|
||||||
|
public_exponent=65537,
|
||||||
|
key_size=key_size,
|
||||||
|
backend=default_backend(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_ca_certificate(
|
||||||
|
private_key: rsa.RSAPrivateKey,
|
||||||
|
common_name: str = "Testing CA",
|
||||||
|
days_valid: int = 3650,
|
||||||
|
) -> x509.Certificate:
|
||||||
|
"""Generate a self-signed CA certificate."""
|
||||||
|
subject = issuer = x509.Name(
|
||||||
|
[
|
||||||
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
||||||
|
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Development"),
|
||||||
|
x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"),
|
||||||
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Testing Organization"),
|
||||||
|
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
certificate = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(subject)
|
||||||
|
.issuer_name(issuer)
|
||||||
|
.public_key(private_key.public_key())
|
||||||
|
.serial_number(x509.random_serial_number())
|
||||||
|
.not_valid_before(datetime.utcnow())
|
||||||
|
.not_valid_after(datetime.utcnow() + timedelta(days=days_valid))
|
||||||
|
.add_extension(
|
||||||
|
x509.BasicConstraints(ca=True, path_length=None),
|
||||||
|
critical=True,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.KeyUsage(
|
||||||
|
digital_signature=True,
|
||||||
|
key_cert_sign=True,
|
||||||
|
crl_sign=True,
|
||||||
|
key_encipherment=False,
|
||||||
|
content_commitment=False,
|
||||||
|
data_encipherment=False,
|
||||||
|
key_agreement=False,
|
||||||
|
encipher_only=False,
|
||||||
|
decipher_only=False,
|
||||||
|
),
|
||||||
|
critical=True,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.sign(private_key, hashes.SHA256(), default_backend())
|
||||||
|
)
|
||||||
|
|
||||||
|
return certificate
|
||||||
|
|
||||||
|
|
||||||
|
def generate_server_certificate(
|
||||||
|
private_key: rsa.RSAPrivateKey,
|
||||||
|
ca_cert: x509.Certificate,
|
||||||
|
ca_key: rsa.RSAPrivateKey,
|
||||||
|
hostname: str = "ldap.testing.local",
|
||||||
|
san_list: list[str] = None,
|
||||||
|
days_valid: int = 365,
|
||||||
|
) -> x509.Certificate:
|
||||||
|
"""Generate a server certificate signed by the CA."""
|
||||||
|
if san_list is None:
|
||||||
|
san_list = [
|
||||||
|
"ldap.testing.local",
|
||||||
|
"localhost",
|
||||||
|
]
|
||||||
|
|
||||||
|
subject = x509.Name(
|
||||||
|
[
|
||||||
|
x509.NameAttribute(NameOID.COUNTRY_NAME, "US"),
|
||||||
|
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "Development"),
|
||||||
|
x509.NameAttribute(NameOID.LOCALITY_NAME, "Local"),
|
||||||
|
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "Testing Organization"),
|
||||||
|
x509.NameAttribute(NameOID.COMMON_NAME, hostname),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build Subject Alternative Names
|
||||||
|
san_entries = []
|
||||||
|
for name in san_list:
|
||||||
|
san_entries.append(x509.DNSName(name))
|
||||||
|
# Add IP addresses
|
||||||
|
san_entries.append(x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")))
|
||||||
|
|
||||||
|
certificate = (
|
||||||
|
x509.CertificateBuilder()
|
||||||
|
.subject_name(subject)
|
||||||
|
.issuer_name(ca_cert.subject)
|
||||||
|
.public_key(private_key.public_key())
|
||||||
|
.serial_number(x509.random_serial_number())
|
||||||
|
.not_valid_before(datetime.utcnow())
|
||||||
|
.not_valid_after(datetime.utcnow() + timedelta(days=days_valid))
|
||||||
|
.add_extension(
|
||||||
|
x509.SubjectAlternativeName(san_entries),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.BasicConstraints(ca=False, path_length=None),
|
||||||
|
critical=True,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.KeyUsage(
|
||||||
|
digital_signature=True,
|
||||||
|
key_encipherment=True,
|
||||||
|
key_cert_sign=False,
|
||||||
|
crl_sign=False,
|
||||||
|
content_commitment=False,
|
||||||
|
data_encipherment=False,
|
||||||
|
key_agreement=False,
|
||||||
|
encipher_only=False,
|
||||||
|
decipher_only=False,
|
||||||
|
),
|
||||||
|
critical=True,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.ExtendedKeyUsage([x509.ExtendedKeyUsageOID.SERVER_AUTH]),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.add_extension(
|
||||||
|
x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
|
||||||
|
critical=False,
|
||||||
|
)
|
||||||
|
.sign(ca_key, hashes.SHA256(), default_backend())
|
||||||
|
)
|
||||||
|
|
||||||
|
return certificate
|
||||||
|
|
||||||
|
|
||||||
|
def save_private_key(key: rsa.RSAPrivateKey, filepath: Path) -> None:
|
||||||
|
"""Save a private key to a file."""
|
||||||
|
pem = key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption(),
|
||||||
|
)
|
||||||
|
filepath.write_bytes(pem)
|
||||||
|
filepath.chmod(0o600) # Set restrictive permissions
|
||||||
|
print(f"✓ Private key saved: {filepath}")
|
||||||
|
|
||||||
|
|
||||||
|
def save_certificate(cert: x509.Certificate, filepath: Path) -> None:
|
||||||
|
"""Save a certificate to a file."""
|
||||||
|
pem = cert.public_bytes(serialization.Encoding.PEM)
|
||||||
|
filepath.write_bytes(pem)
|
||||||
|
filepath.chmod(0o644)
|
||||||
|
print(f"✓ Certificate saved: {filepath}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Generate SSL/TLS certificates for LDAP development server"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--output-dir",
|
||||||
|
type=Path,
|
||||||
|
default=Path(__file__).parent.parent / "certs",
|
||||||
|
help="Output directory for certificates (default: ./certs)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--hostname",
|
||||||
|
default="ldap.testing.local",
|
||||||
|
help="Server hostname (default: ldap.testing.local)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--san",
|
||||||
|
action="append",
|
||||||
|
help="Additional Subject Alternative Names (can be used multiple times)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--force",
|
||||||
|
action="store_true",
|
||||||
|
help="Overwrite existing certificates",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--ca-days",
|
||||||
|
type=int,
|
||||||
|
default=3650,
|
||||||
|
help="CA certificate validity in days (default: 3650)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--server-days",
|
||||||
|
type=int,
|
||||||
|
default=365,
|
||||||
|
help="Server certificate validity in days (default: 365)",
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Ensure output directory exists
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Define file paths
|
||||||
|
ca_key_path = args.output_dir / "ca.key"
|
||||||
|
ca_cert_path = args.output_dir / "ca.crt"
|
||||||
|
server_key_path = args.output_dir / "server.key"
|
||||||
|
server_cert_path = args.output_dir / "server.crt"
|
||||||
|
|
||||||
|
# Check if certificates already exist
|
||||||
|
if not args.force:
|
||||||
|
existing = [
|
||||||
|
p for p in [ca_cert_path, server_cert_path, server_key_path] if p.exists()
|
||||||
|
]
|
||||||
|
if existing:
|
||||||
|
print("Error: The following certificate files already exist:")
|
||||||
|
for p in existing:
|
||||||
|
print(f" - {p}")
|
||||||
|
print("\nUse --force to overwrite existing certificates")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Generating certificates for LDAP server...")
|
||||||
|
print(f"Hostname: {args.hostname}")
|
||||||
|
print(f"Output directory: {args.output_dir}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Generate CA
|
||||||
|
print("Step 1: Generating CA certificate...")
|
||||||
|
ca_key = generate_private_key()
|
||||||
|
ca_cert = generate_ca_certificate(ca_key, days_valid=args.ca_days)
|
||||||
|
save_private_key(ca_key, ca_key_path)
|
||||||
|
save_certificate(ca_cert, ca_cert_path)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Generate Server Certificate
|
||||||
|
print("Step 2: Generating server certificate...")
|
||||||
|
server_key = generate_private_key()
|
||||||
|
|
||||||
|
san_list = [args.hostname, "localhost"]
|
||||||
|
if args.san:
|
||||||
|
san_list.extend(args.san)
|
||||||
|
|
||||||
|
server_cert = generate_server_certificate(
|
||||||
|
server_key,
|
||||||
|
ca_cert,
|
||||||
|
ca_key,
|
||||||
|
hostname=args.hostname,
|
||||||
|
san_list=san_list,
|
||||||
|
days_valid=args.server_days,
|
||||||
|
)
|
||||||
|
save_private_key(server_key, server_key_path)
|
||||||
|
save_certificate(server_cert, server_cert_path)
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("✅ Certificate generation complete!")
|
||||||
|
print()
|
||||||
|
print("Generated files:")
|
||||||
|
print(f" - CA Certificate: {ca_cert_path}")
|
||||||
|
print(f" - CA Private Key: {ca_key_path} (keep secure!)")
|
||||||
|
print(f" - Server Certificate: {server_cert_path}")
|
||||||
|
print(f" - Server Private Key: {server_key_path} (keep secure!)")
|
||||||
|
print()
|
||||||
|
print("Next steps:")
|
||||||
|
print(" 1. Keep the CA private key secure (you can delete it if not needed)")
|
||||||
|
print(" 2. Start the LDAP server: docker-compose up -d")
|
||||||
|
print(" 3. Test the connection: ldapsearch -H ldaps://localhost:636 -x")
|
||||||
|
print()
|
||||||
|
print("Note: These certificates are for DEVELOPMENT ONLY!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
8
tests/__init__.py
Normal file
8
tests/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
Tests for LDAP Docker Management Tools
|
||||||
|
|
||||||
|
This package contains unit and integration tests for the LDAP Docker
|
||||||
|
development environment management scripts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
252
tests/test_generate_certs.py
Normal file
252
tests/test_generate_certs.py
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
"""
|
||||||
|
Tests for certificate generation script.
|
||||||
|
|
||||||
|
This module contains unit tests for the generate_certs.py script.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
|
||||||
|
CRYPTO_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
CRYPTO_AVAILABLE = False
|
||||||
|
|
||||||
|
# Import the module to test
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||||
|
|
||||||
|
if CRYPTO_AVAILABLE:
|
||||||
|
from scripts.generate_certs import (
|
||||||
|
generate_ca_certificate,
|
||||||
|
generate_private_key,
|
||||||
|
generate_server_certificate,
|
||||||
|
save_certificate,
|
||||||
|
save_private_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not CRYPTO_AVAILABLE, reason="cryptography library not available")
|
||||||
|
class TestCertificateGeneration:
|
||||||
|
"""Tests for certificate generation functions."""
|
||||||
|
|
||||||
|
def test_generate_private_key(self):
|
||||||
|
"""Test that a private key can be generated."""
|
||||||
|
key = generate_private_key(key_size=2048)
|
||||||
|
assert isinstance(key, rsa.RSAPrivateKey)
|
||||||
|
assert key.key_size == 2048
|
||||||
|
|
||||||
|
def test_generate_private_key_default_size(self):
|
||||||
|
"""Test that default key size is 4096."""
|
||||||
|
key = generate_private_key()
|
||||||
|
assert isinstance(key, rsa.RSAPrivateKey)
|
||||||
|
assert key.key_size == 4096
|
||||||
|
|
||||||
|
def test_generate_ca_certificate(self):
|
||||||
|
"""Test that a CA certificate can be generated."""
|
||||||
|
key = generate_private_key(key_size=2048)
|
||||||
|
cert = generate_ca_certificate(key, common_name="Test CA", days_valid=365)
|
||||||
|
|
||||||
|
assert isinstance(cert, x509.Certificate)
|
||||||
|
assert cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value == "Test CA"
|
||||||
|
|
||||||
|
# Check that it's self-signed
|
||||||
|
assert cert.subject == cert.issuer
|
||||||
|
|
||||||
|
# Check basic constraints
|
||||||
|
basic_constraints = cert.extensions.get_extension_for_oid(
|
||||||
|
x509.ExtensionOID.BASIC_CONSTRAINTS
|
||||||
|
)
|
||||||
|
assert basic_constraints.value.ca is True
|
||||||
|
|
||||||
|
def test_generate_ca_certificate_validity(self):
|
||||||
|
"""Test that CA certificate has correct validity period."""
|
||||||
|
key = generate_private_key(key_size=2048)
|
||||||
|
days_valid = 365
|
||||||
|
cert = generate_ca_certificate(key, days_valid=days_valid)
|
||||||
|
|
||||||
|
now = datetime.utcnow()
|
||||||
|
expected_expiry = now + timedelta(days=days_valid)
|
||||||
|
|
||||||
|
# Allow 1 minute tolerance for test execution time
|
||||||
|
assert abs((cert.not_valid_after - expected_expiry).total_seconds()) < 60
|
||||||
|
|
||||||
|
def test_generate_server_certificate(self):
|
||||||
|
"""Test that a server certificate can be generated."""
|
||||||
|
ca_key = generate_private_key(key_size=2048)
|
||||||
|
ca_cert = generate_ca_certificate(ca_key, common_name="Test CA")
|
||||||
|
|
||||||
|
server_key = generate_private_key(key_size=2048)
|
||||||
|
server_cert = generate_server_certificate(
|
||||||
|
server_key,
|
||||||
|
ca_cert,
|
||||||
|
ca_key,
|
||||||
|
hostname="test.example.com",
|
||||||
|
days_valid=365,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert isinstance(server_cert, x509.Certificate)
|
||||||
|
assert (
|
||||||
|
server_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value
|
||||||
|
== "test.example.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that it's signed by the CA
|
||||||
|
assert server_cert.issuer == ca_cert.subject
|
||||||
|
|
||||||
|
# Check basic constraints - should not be a CA
|
||||||
|
basic_constraints = server_cert.extensions.get_extension_for_oid(
|
||||||
|
x509.ExtensionOID.BASIC_CONSTRAINTS
|
||||||
|
)
|
||||||
|
assert basic_constraints.value.ca is False
|
||||||
|
|
||||||
|
def test_generate_server_certificate_san(self):
|
||||||
|
"""Test that server certificate includes Subject Alternative Names."""
|
||||||
|
ca_key = generate_private_key(key_size=2048)
|
||||||
|
ca_cert = generate_ca_certificate(ca_key)
|
||||||
|
|
||||||
|
server_key = generate_private_key(key_size=2048)
|
||||||
|
san_list = ["test.example.com", "localhost", "test.local"]
|
||||||
|
server_cert = generate_server_certificate(
|
||||||
|
server_key,
|
||||||
|
ca_cert,
|
||||||
|
ca_key,
|
||||||
|
hostname="test.example.com",
|
||||||
|
san_list=san_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get SAN extension
|
||||||
|
san_ext = server_cert.extensions.get_extension_for_oid(
|
||||||
|
x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract DNS names
|
||||||
|
dns_names = [name.value for name in san_ext.value if isinstance(name, x509.DNSName)]
|
||||||
|
|
||||||
|
# Check that all DNS names are present
|
||||||
|
for name in san_list:
|
||||||
|
assert name in dns_names
|
||||||
|
|
||||||
|
def test_save_private_key(self):
|
||||||
|
"""Test that a private key can be saved to a file."""
|
||||||
|
key = generate_private_key(key_size=2048)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
key_path = Path(tmpdir) / "test.key"
|
||||||
|
save_private_key(key, key_path)
|
||||||
|
|
||||||
|
assert key_path.exists()
|
||||||
|
assert key_path.stat().st_size > 0
|
||||||
|
|
||||||
|
# Check file permissions (on Unix-like systems)
|
||||||
|
if hasattr(key_path.stat(), "st_mode"):
|
||||||
|
mode = key_path.stat().st_mode & 0o777
|
||||||
|
assert mode == 0o600, f"Expected 0o600 but got {oct(mode)}"
|
||||||
|
|
||||||
|
def test_save_certificate(self):
|
||||||
|
"""Test that a certificate can be saved to a file."""
|
||||||
|
key = generate_private_key(key_size=2048)
|
||||||
|
cert = generate_ca_certificate(key)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
cert_path = Path(tmpdir) / "test.crt"
|
||||||
|
save_certificate(cert, cert_path)
|
||||||
|
|
||||||
|
assert cert_path.exists()
|
||||||
|
assert cert_path.stat().st_size > 0
|
||||||
|
|
||||||
|
# Check file permissions
|
||||||
|
if hasattr(cert_path.stat(), "st_mode"):
|
||||||
|
mode = cert_path.stat().st_mode & 0o777
|
||||||
|
assert mode == 0o644, f"Expected 0o644 but got {oct(mode)}"
|
||||||
|
|
||||||
|
def test_full_certificate_chain(self):
|
||||||
|
"""Test generating a complete certificate chain."""
|
||||||
|
# Generate CA
|
||||||
|
ca_key = generate_private_key(key_size=2048)
|
||||||
|
ca_cert = generate_ca_certificate(ca_key, common_name="Test Root CA")
|
||||||
|
|
||||||
|
# Generate server certificate
|
||||||
|
server_key = generate_private_key(key_size=2048)
|
||||||
|
server_cert = generate_server_certificate(
|
||||||
|
server_key,
|
||||||
|
ca_cert,
|
||||||
|
ca_key,
|
||||||
|
hostname="ldap.testing.local",
|
||||||
|
san_list=["ldap.testing.local", "localhost"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify the chain
|
||||||
|
assert server_cert.issuer == ca_cert.subject
|
||||||
|
assert ca_cert.subject == ca_cert.issuer # Self-signed
|
||||||
|
|
||||||
|
# Verify server cert is not a CA
|
||||||
|
server_basic_constraints = server_cert.extensions.get_extension_for_oid(
|
||||||
|
x509.ExtensionOID.BASIC_CONSTRAINTS
|
||||||
|
)
|
||||||
|
assert server_basic_constraints.value.ca is False
|
||||||
|
|
||||||
|
# Verify CA cert is a CA
|
||||||
|
ca_basic_constraints = ca_cert.extensions.get_extension_for_oid(
|
||||||
|
x509.ExtensionOID.BASIC_CONSTRAINTS
|
||||||
|
)
|
||||||
|
assert ca_basic_constraints.value.ca is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not CRYPTO_AVAILABLE, reason="cryptography library not available")
|
||||||
|
class TestCertificateValidation:
|
||||||
|
"""Tests for certificate validation and properties."""
|
||||||
|
|
||||||
|
def test_certificate_has_correct_extensions(self):
|
||||||
|
"""Test that generated certificates have correct extensions."""
|
||||||
|
ca_key = generate_private_key(key_size=2048)
|
||||||
|
ca_cert = generate_ca_certificate(ca_key)
|
||||||
|
|
||||||
|
server_key = generate_private_key(key_size=2048)
|
||||||
|
server_cert = generate_server_certificate(
|
||||||
|
server_key, ca_cert, ca_key, hostname="test.local"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check server certificate extensions
|
||||||
|
ext_oids = [ext.oid for ext in server_cert.extensions]
|
||||||
|
|
||||||
|
assert x509.ExtensionOID.SUBJECT_ALTERNATIVE_NAME in ext_oids
|
||||||
|
assert x509.ExtensionOID.BASIC_CONSTRAINTS in ext_oids
|
||||||
|
assert x509.ExtensionOID.KEY_USAGE in ext_oids
|
||||||
|
assert x509.ExtensionOID.EXTENDED_KEY_USAGE in ext_oids
|
||||||
|
assert x509.ExtensionOID.SUBJECT_KEY_IDENTIFIER in ext_oids
|
||||||
|
assert x509.ExtensionOID.AUTHORITY_KEY_IDENTIFIER in ext_oids
|
||||||
|
|
||||||
|
def test_certificate_validity_dates(self):
|
||||||
|
"""Test that certificates have correct validity dates."""
|
||||||
|
key = generate_private_key(key_size=2048)
|
||||||
|
days_valid = 100
|
||||||
|
cert = generate_ca_certificate(key, days_valid=days_valid)
|
||||||
|
|
||||||
|
now = datetime.utcnow()
|
||||||
|
|
||||||
|
# Check not_valid_before is around now
|
||||||
|
assert abs((cert.not_valid_before - now).total_seconds()) < 60
|
||||||
|
|
||||||
|
# Check not_valid_after is around now + days_valid
|
||||||
|
expected_expiry = now + timedelta(days=days_valid)
|
||||||
|
assert abs((cert.not_valid_after - expected_expiry).total_seconds()) < 60
|
||||||
|
|
||||||
|
|
||||||
|
def test_imports():
|
||||||
|
"""Test that all required imports are available."""
|
||||||
|
if CRYPTO_AVAILABLE:
|
||||||
|
assert x509 is not None
|
||||||
|
assert hashes is not None
|
||||||
|
assert rsa is not None
|
||||||
|
else:
|
||||||
|
pytest.skip("cryptography library not available")
|
||||||
Reference in New Issue
Block a user