adding features
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# Configuración para Docker Hub y GitHub Actions
|
||||
|
||||
Este documento explica cómo configurar los secretos necesarios para publicar imágenes de Docker en Docker Hub usando GitHub Actions.
|
||||
|
||||
## Requisitos previos
|
||||
|
||||
1. Una cuenta en [Docker Hub](https://hub.docker.com/)
|
||||
2. Un repositorio en Docker Hub donde subir la imagen
|
||||
3. Un token de acceso personal (PAT) de Docker Hub
|
||||
|
||||
## Pasos para configurar los secretos en GitHub
|
||||
|
||||
1. Genera un token de acceso en Docker Hub
|
||||
- Inicia sesión en [Docker Hub](https://hub.docker.com/)
|
||||
- Ve a tu perfil (esquina superior derecha) → Account Settings → Security
|
||||
- Haz clic en "New Access Token"
|
||||
- Proporciona una descripción como "GitHub Actions"
|
||||
- Selecciona los permisos apropiados (normalmente "Read, Write, Delete")
|
||||
- Haz clic en "Generate"
|
||||
- **IMPORTANTE**: Copia el token generado, ya que no podrás verlo de nuevo
|
||||
|
||||
2. Configura los secretos en tu repositorio de GitHub
|
||||
- Ve a tu repositorio en GitHub
|
||||
- Haz clic en "Settings" → "Secrets and variables" → "Actions"
|
||||
- Haz clic en "New repository secret"
|
||||
- Añade los siguientes secretos:
|
||||
- Nombre: `DOCKERHUB_USERNAME`
|
||||
Valor: Tu nombre de usuario de Docker Hub
|
||||
- Nombre: `DOCKERHUB_TOKEN`
|
||||
Valor: El token de acceso que generaste en el paso anterior
|
||||
|
||||
## Uso
|
||||
|
||||
Una vez configurados los secretos, los flujos de trabajo de GitHub Actions podrán autenticarse con Docker Hub y publicar imágenes.
|
||||
|
||||
Cuando crees una nueva [release en GitHub](https://docs.github.com/es/repositories/releasing-projects-on-github/managing-releases-in-a-repository#creating-a-release), el flujo de trabajo `docker-publish.yml` se activará automáticamente y:
|
||||
|
||||
1. Construirá la imagen de Docker
|
||||
2. La etiquetará con el número de versión de la release
|
||||
3. La subirá a Docker Hub
|
||||
|
||||
## Verificación
|
||||
|
||||
Para verificar que la configuración está correcta:
|
||||
|
||||
1. Crea una nueva release en GitHub
|
||||
2. Ve a la pestaña "Actions" y observa el progreso del flujo de trabajo
|
||||
3. Una vez completado, verifica que la imagen aparezca en tu repositorio de Docker Hub
|
||||
|
||||
## Notas adicionales
|
||||
|
||||
- Para entornos de producción, considera usar un usuario de servicio en Docker Hub en lugar de tu cuenta personal
|
||||
- Rota regularmente los tokens de acceso para mayor seguridad
|
||||
- Considera agregar escaneo de vulnerabilidades en las imágenes como parte del flujo de trabajo
|
||||
@@ -0,0 +1,56 @@
|
||||
name: Docker Build and Test
|
||||
|
||||
# Trigger on push to main branch or on pull requests
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "dev" ]
|
||||
pull_request:
|
||||
branches: [ "main", "dev" ]
|
||||
|
||||
jobs:
|
||||
# Build and test Docker image but don't push
|
||||
build-and-test:
|
||||
name: Build and Test Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up Docker Buildx
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: test/oxicloud
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=sha
|
||||
|
||||
# Build Docker image but don't push
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: false
|
||||
load: true
|
||||
tags: test/oxicloud:test
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
# Run some basic tests against the built image
|
||||
- name: Test Docker image
|
||||
run: |
|
||||
docker run --rm test/oxicloud:test --version || true
|
||||
docker run --rm test/oxicloud:test --help || true
|
||||
|
||||
# Verify the image structure
|
||||
echo "✅ Checking Docker image layers and size"
|
||||
docker image inspect test/oxicloud:test
|
||||
|
||||
echo "✅ Docker build and test completed successfully"
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Docker Hub Release
|
||||
|
||||
# Trigger the workflow when a release is published
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
# Build and publish Docker image
|
||||
build-and-push:
|
||||
name: Build and Push Docker Image
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Set up Docker Buildx for efficient builds
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
# Login to Docker Hub
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# Extract metadata (tags, labels) for Docker
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/oxicloud
|
||||
# Generate Docker tags based on release tag, commit SHA, and latest
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=ref,event=branch
|
||||
type=sha
|
||||
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
# Build and push Docker image
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
platforms: linux/amd64,linux/arm64
|
||||
# Build args if needed
|
||||
build-args: |
|
||||
VERSION=${{ github.ref_name }}
|
||||
|
||||
# Post successful build notification
|
||||
- name: Post Success Notification
|
||||
if: success()
|
||||
run: |
|
||||
echo "🚢 Docker image for version ${{ github.ref_name }} has been successfully pushed to Docker Hub!"
|
||||
@@ -0,0 +1,60 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build/Lint/Test Commands
|
||||
|
||||
### Building
|
||||
- Build debug version: `cargo build`
|
||||
- Build release version: `cargo build --release`
|
||||
- Run the application: `cargo run`
|
||||
|
||||
### Testing
|
||||
- Run all tests: `cargo test`
|
||||
- Run a specific test: `cargo test test_name`
|
||||
- Run tests for a specific module: `cargo test module_name`
|
||||
- Run tests with feature flags: `cargo test --features test_utils`
|
||||
|
||||
### Linting
|
||||
- Run clippy linting: `cargo clippy`
|
||||
- Format code: `cargo fmt`
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Architecture
|
||||
- This project follows a hexagonal/clean architecture pattern:
|
||||
- `application`: Contains services (use cases), DTOs, and ports
|
||||
- `domain`: Contains core entities, repositories (interfaces), and domain services
|
||||
- `infrastructure`: Contains concrete implementations of repository interfaces
|
||||
- `interfaces`: Contains HTTP/API handlers and routes
|
||||
|
||||
### Error Handling
|
||||
- Use the `DomainError` type for domain-level errors
|
||||
- Use the `AppError` type for API/HTTP-level errors
|
||||
- Use the `ErrorContext` trait to add context to errors from external crates
|
||||
- Follow the error factory pattern for creating common error types
|
||||
|
||||
### Naming Conventions
|
||||
- Types and structs: PascalCase
|
||||
- Functions and methods: snake_case
|
||||
- Constants and statics: SCREAMING_SNAKE_CASE
|
||||
- Modules and files: snake_case
|
||||
- Use descriptive names that express intent
|
||||
|
||||
### Testing
|
||||
- Use mock objects for dependencies in unit tests
|
||||
- Use the `#[tokio::test]` attribute for async tests
|
||||
- Include both positive and negative test cases
|
||||
- Follow the Arrange-Act-Assert pattern in tests
|
||||
|
||||
### Imports
|
||||
- Group imports by source:
|
||||
1. Standard library imports
|
||||
2. External crate imports
|
||||
3. Local crate imports (with `crate::` prefix)
|
||||
- Use explicit imports (no glob imports except in tests)
|
||||
|
||||
### Documentation
|
||||
- Document public API functions and types with doc comments
|
||||
- Include examples where helpful
|
||||
- Document error cases and conditions
|
||||
@@ -0,0 +1,246 @@
|
||||
# Configuración de Clientes DAV para OxiCloud
|
||||
|
||||
Esta guía proporciona instrucciones para configurar varios clientes que soportan WebDAV, CalDAV y CardDAV para conectar con OxiCloud.
|
||||
|
||||
## Tabla de Contenidos
|
||||
|
||||
1. [URLs de Conexión](#urls-de-conexión)
|
||||
2. [Clientes WebDAV](#clientes-webdav)
|
||||
3. [Clientes CalDAV](#clientes-caldav)
|
||||
4. [Clientes CardDAV](#clientes-carddav)
|
||||
5. [Solución de Problemas](#solución-de-problemas)
|
||||
|
||||
## URLs de Conexión
|
||||
|
||||
Usa las siguientes URLs para conectar tus clientes con OxiCloud:
|
||||
|
||||
- **WebDAV**: `https://tu-servidor.com/webdav/`
|
||||
- **CalDAV**:
|
||||
- Principal: `https://tu-servidor.com/caldav/`
|
||||
- Calendario específico: `https://tu-servidor.com/caldav/{nombre-calendario}/`
|
||||
- **CardDAV**:
|
||||
- Principal: `https://tu-servidor.com/carddav/addressbooks/`
|
||||
- Libreta específica: `https://tu-servidor.com/carddav/addressbooks/{nombre-libreta}/`
|
||||
|
||||
## Clientes WebDAV
|
||||
|
||||
### Windows
|
||||
|
||||
#### Windows Explorer
|
||||
|
||||
1. Abre el Explorador de Windows
|
||||
2. Haz clic derecho en "Este equipo" y selecciona "Agregar una ubicación de red"
|
||||
3. Haz clic en "Siguiente"
|
||||
4. Selecciona "Elegir una ubicación de red personalizada" y haz clic en "Siguiente"
|
||||
5. En el campo de dirección, introduce: `https://tu-servidor.com/webdav/`
|
||||
6. Haz clic en "Siguiente"
|
||||
7. Introduce un nombre para la conexión (ej. "OxiCloud")
|
||||
8. Haz clic en "Siguiente" y luego en "Finalizar"
|
||||
9. Introduce tus credenciales cuando se te soliciten
|
||||
|
||||
#### Problemas Comunes en Windows
|
||||
|
||||
- **Error de SSL**: Asegúrate de que tu certificado SSL sea válido y confiable para Windows
|
||||
- **Bloqueo por WebClient**: Asegúrate de que el servicio "WebClient" de Windows esté activado
|
||||
- **Límite de tamaño**: Windows limita por defecto las cargas a 50MB, modifica el registro para aumentarlo:
|
||||
|
||||
```
|
||||
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters]
|
||||
"FileSizeLimitInBytes"=dword:00FFFFFF
|
||||
```
|
||||
|
||||
### macOS
|
||||
|
||||
#### Finder
|
||||
|
||||
1. En Finder, haz clic en "Ir" en la barra de menú
|
||||
2. Selecciona "Conectar al servidor..." o presiona ⌘+K
|
||||
3. Introduce `https://tu-servidor.com/webdav/` como dirección del servidor
|
||||
4. Haz clic en "Conectar"
|
||||
5. Introduce tus credenciales cuando se te soliciten
|
||||
6. Selecciona si deseas guardar la contraseña en el llavero
|
||||
|
||||
### Linux
|
||||
|
||||
#### GNOME Files (Nautilus)
|
||||
|
||||
1. Abre Nautilus (Archivos)
|
||||
2. Haz clic en "Otras ubicaciones" en el panel lateral
|
||||
3. En la parte inferior, introduce `davs://tu-servidor.com/webdav/` en "Conectar al servidor"
|
||||
4. Haz clic en "Conectar"
|
||||
5. Introduce tus credenciales cuando se te soliciten
|
||||
|
||||
#### Dolphin (KDE)
|
||||
|
||||
1. Abre Dolphin
|
||||
2. En la barra de dirección, escribe `webdavs://tu-servidor.com/webdav/`
|
||||
3. Introduce tus credenciales cuando se te soliciten
|
||||
|
||||
### Clientes Multiplataforma
|
||||
|
||||
#### Cyberduck
|
||||
|
||||
1. Descarga e instala [Cyberduck](https://cyberduck.io/)
|
||||
2. Haz clic en "Nueva conexión"
|
||||
3. Selecciona "WebDAV (HTTP/SSL)" como tipo de conexión
|
||||
4. Introduce los siguientes datos:
|
||||
- Servidor: `tu-servidor.com`
|
||||
- Puerto: `443`
|
||||
- Ruta: `/webdav/`
|
||||
- Nombre de usuario: tu nombre de usuario
|
||||
- Contraseña: tu contraseña
|
||||
5. Haz clic en "Conectar"
|
||||
|
||||
## Clientes CalDAV
|
||||
|
||||
### Apple Calendar (macOS/iOS)
|
||||
|
||||
#### macOS
|
||||
|
||||
1. Abre la aplicación Calendario
|
||||
2. Haz clic en "Calendario" en la barra de menú
|
||||
3. Selecciona "Añadir cuenta..."
|
||||
4. Selecciona "Otra cuenta de CalDAV..."
|
||||
5. Completa la información:
|
||||
- Correo electrónico: tu dirección de correo
|
||||
- Contraseña: tu contraseña
|
||||
- Dirección del servidor: `tu-servidor.com`
|
||||
- Ruta: `/caldav/` (deja en blanco si no funciona)
|
||||
6. Haz clic en "Iniciar sesión"
|
||||
|
||||
#### iOS
|
||||
|
||||
1. Ve a Ajustes > Calendario > Cuentas > Añadir cuenta
|
||||
2. Selecciona "Otra"
|
||||
3. Selecciona "Añadir cuenta CalDAV"
|
||||
4. Completa la información:
|
||||
- Servidor: `https://tu-servidor.com/caldav/`
|
||||
- Nombre de usuario: tu nombre de usuario
|
||||
- Contraseña: tu contraseña
|
||||
- Descripción: "OxiCloud Calendario"
|
||||
5. Toca "Siguiente" y luego "Guardar"
|
||||
|
||||
### Mozilla Thunderbird con Lightning
|
||||
|
||||
1. Instala Thunderbird y la extensión Lightning
|
||||
2. Haz clic en el botón de calendario en la barra lateral
|
||||
3. Haz clic derecho en el panel izquierdo y selecciona "Nuevo calendario"
|
||||
4. Selecciona "En la red" y haz clic en "Siguiente"
|
||||
5. Selecciona "CalDAV" como formato
|
||||
6. Introduce `https://tu-servidor.com/caldav/nombre-calendario/` como ubicación
|
||||
7. Haz clic en "Siguiente", introduce un nombre para el calendario
|
||||
8. Completa la configuración y haz clic en "Finalizar"
|
||||
9. Introduce tus credenciales cuando se te soliciten
|
||||
|
||||
### Nextcloud Desktop Sync
|
||||
|
||||
1. Descarga e instala el cliente de sincronización de Nextcloud
|
||||
2. Durante la configuración, selecciona "Solo sincronización de calendario y contactos"
|
||||
3. Introduce `https://tu-servidor.com` como dirección del servidor
|
||||
4. Introduce tus credenciales
|
||||
5. En las opciones de sincronización, selecciona los calendarios que deseas sincronizar
|
||||
|
||||
## Clientes CardDAV
|
||||
|
||||
### Apple Contacts (macOS/iOS)
|
||||
|
||||
#### macOS
|
||||
|
||||
1. Abre la aplicación Contactos
|
||||
2. Haz clic en "Contactos" en la barra de menú
|
||||
3. Selecciona "Añadir cuenta..."
|
||||
4. Selecciona "Otra cuenta de CardDAV..."
|
||||
5. Completa la información:
|
||||
- Correo electrónico: tu dirección de correo
|
||||
- Contraseña: tu contraseña
|
||||
- Dirección del servidor: `tu-servidor.com`
|
||||
- Ruta: `/carddav/addressbooks/` (deja en blanco si no funciona)
|
||||
6. Haz clic en "Iniciar sesión"
|
||||
|
||||
#### iOS
|
||||
|
||||
1. Ve a Ajustes > Contactos > Cuentas > Añadir cuenta
|
||||
2. Selecciona "Otra"
|
||||
3. Selecciona "Añadir cuenta CardDAV"
|
||||
4. Completa la información:
|
||||
- Servidor: `https://tu-servidor.com/carddav/addressbooks/`
|
||||
- Nombre de usuario: tu nombre de usuario
|
||||
- Contraseña: tu contraseña
|
||||
- Descripción: "OxiCloud Contactos"
|
||||
5. Toca "Siguiente" y luego "Guardar"
|
||||
|
||||
### Mozilla Thunderbird
|
||||
|
||||
1. Instala Thunderbird y la extensión CardBook
|
||||
2. Abre CardBook desde el menú de Thunderbird
|
||||
3. Haz clic en "Libreta de direcciones" > "Nuevo" > "Libreta de direcciones remota"
|
||||
4. Selecciona "CardDAV" como tipo
|
||||
5. Introduce `https://tu-servidor.com/carddav/addressbooks/nombre-libreta/` como URL
|
||||
6. Introduce un nombre para la libreta de direcciones
|
||||
7. Introduce tus credenciales
|
||||
8. Haz clic en "Validar" y luego en "Aceptar"
|
||||
|
||||
### Cliente Evolution (Linux)
|
||||
|
||||
1. Abre Evolution
|
||||
2. Ve a Archivo > Nuevo > Libreta de direcciones
|
||||
3. Selecciona "CardDAV" como tipo
|
||||
4. Introduce `https://tu-servidor.com/carddav/addressbooks/nombre-libreta/` como URL
|
||||
5. Introduce un nombre para la libreta de direcciones
|
||||
6. Introduce tus credenciales
|
||||
7. Haz clic en "Aplicar"
|
||||
|
||||
## Solución de Problemas
|
||||
|
||||
### Problemas Comunes
|
||||
|
||||
1. **Error de autenticación**
|
||||
- Verifica que estés usando las credenciales correctas
|
||||
- Asegúrate de que tu cuenta tenga acceso a los recursos DAV
|
||||
- Si usas autenticación de dos factores, es posible que necesites crear una contraseña de aplicación específica
|
||||
|
||||
2. **No se pueden encontrar calendarios/libretas**
|
||||
- Verifica que hayas creado calendarios o libretas de direcciones en OxiCloud
|
||||
- Asegúrate de estar usando la URL correcta, incluyendo la terminación con `/`
|
||||
- Verifica los permisos de los recursos
|
||||
|
||||
3. **Error SSL/TLS**
|
||||
- Asegúrate de que tu certificado SSL sea válido y confiable
|
||||
- Verifica que la fecha y hora de tu dispositivo sean correctas
|
||||
- En algunos clientes, puede ser necesario confiar manualmente en el certificado
|
||||
|
||||
4. **Sincronización lenta**
|
||||
- Limita el número de elementos en tus calendarios y libretas de direcciones
|
||||
- Verifica la calidad de tu conexión a Internet
|
||||
- Algunas operaciones masivas (como importar muchos contactos) pueden tardar tiempo
|
||||
|
||||
### Herramientas de Diagnóstico
|
||||
|
||||
1. **Verificación de conectividad**: Prueba la conexión básica con:
|
||||
```
|
||||
curl -v https://tu-servidor.com/webdav/
|
||||
```
|
||||
|
||||
2. **Prueba de autenticación**:
|
||||
```
|
||||
curl -v -u usuario:contraseña https://tu-servidor.com/webdav/
|
||||
```
|
||||
|
||||
3. **Prueba de funcionalidad WebDAV**:
|
||||
```
|
||||
curl -X PROPFIND -H "Depth: 1" -u usuario:contraseña https://tu-servidor.com/webdav/
|
||||
```
|
||||
|
||||
4. **Prueba de funcionalidad CalDAV**:
|
||||
```
|
||||
curl -X PROPFIND -H "Depth: 1" -u usuario:contraseña https://tu-servidor.com/caldav/
|
||||
```
|
||||
|
||||
5. **Logs del servidor**: Si tienes acceso, revisa los logs del servidor para identificar problemas específicos.
|
||||
|
||||
### Contacto para Soporte
|
||||
|
||||
Si continúas experimentando problemas, contacta con soporte en:
|
||||
- Email: soporte@ejemplo.com
|
||||
- Foro: https://ejemplo.com/foro
|
||||
- Sistema de tickets: https://soporte.ejemplo.com
|
||||
@@ -0,0 +1,286 @@
|
||||
# Plan de Implementación DAV para OxiCloud
|
||||
|
||||
Este documento presenta un plan de implementación estructurado para añadir soporte WebDAV, CalDAV y CardDAV a OxiCloud.
|
||||
|
||||
## Resumen Ejecutivo
|
||||
|
||||
La implementación de los protocolos DAV (WebDAV, CalDAV y CardDAV) permitirá a OxiCloud interoperar con una amplia gama de clientes y dispositivos, aumentando significativamente su versatilidad y utilidad. Este plan propone un enfoque por fases que prioriza primero WebDAV (para acceso a archivos), seguido de CalDAV (para calendarios) y finalmente CardDAV (para contactos).
|
||||
|
||||
## Fases de Implementación
|
||||
|
||||
### Fase 1: Infraestructura DAV Común (Estimado: 2-3 semanas)
|
||||
|
||||
**Objetivos:**
|
||||
- Establecer la infraestructura básica compartida por todos los protocolos DAV
|
||||
- Implementar el manejo de solicitudes XML y respuestas
|
||||
- Crear adaptadores para las operaciones básicas DAV
|
||||
|
||||
**Tareas:**
|
||||
1. **Semana 1: Diseño y Arquitectura**
|
||||
- Diseñar la arquitectura de los componentes DAV
|
||||
- Definir interfaces para adaptadores DAV
|
||||
- Seleccionar bibliotecas para procesamiento XML y RFC4918
|
||||
|
||||
2. **Semana 2: Implementación Base**
|
||||
- Implementar manejadores de serialización/deserialización XML
|
||||
- Desarrollar middleware para procesamiento de solicitudes DAV
|
||||
- Crear estructuras comunes (propiedades, espacios de nombres)
|
||||
- Implementar validación de solicitudes DAV
|
||||
|
||||
3. **Semana 3: Framework de Pruebas**
|
||||
- Configurar entorno de pruebas para protocolos DAV
|
||||
- Implementar clientes de prueba automatizados
|
||||
- Crear casos de prueba para operaciones DAV básicas
|
||||
|
||||
**Entregables:**
|
||||
- Framework de procesamiento XML para solicitudes/respuestas DAV
|
||||
- Adaptadores base para las entidades existentes
|
||||
- Suite de pruebas para operaciones DAV
|
||||
|
||||
### Fase 2: WebDAV (Estimado: 3-4 semanas)
|
||||
|
||||
**Objetivos:**
|
||||
- Implementar el protocolo WebDAV completo (RFC4918)
|
||||
- Permitir acceso a archivos y carpetas vía WebDAV
|
||||
- Asegurar compatibilidad con clientes WebDAV comunes
|
||||
|
||||
**Tareas:**
|
||||
1. **Semana 1: Operaciones Básicas**
|
||||
- Implementar métodos PROPFIND y PROPPATCH
|
||||
- Desarrollar endpoint OPTIONS (descubrimiento de capacidades)
|
||||
- Implementar operaciones GET, HEAD, PUT (lectura/escritura)
|
||||
|
||||
2. **Semana 2: Operaciones Avanzadas**
|
||||
- Implementar MKCOL (creación de directorios)
|
||||
- Desarrollar DELETE para recursos WebDAV
|
||||
- Implementar COPY y MOVE para archivos y directorios
|
||||
|
||||
3. **Semana 3: Bloqueo y Características Extendidas**
|
||||
- Implementar LOCK y UNLOCK para recursos
|
||||
- Añadir soporte para propiedades personalizadas
|
||||
- Desarrollar características de WebDAV extendidas (si es necesario)
|
||||
|
||||
4. **Semana 4: Pruebas y Optimización**
|
||||
- Realizar pruebas con clientes reales (Windows, macOS, Linux)
|
||||
- Optimizar rendimiento para transferencias grandes
|
||||
- Documentar APIs y comportamiento WebDAV
|
||||
|
||||
**Entregables:**
|
||||
- Implementación completa de WebDAV (RFC4918)
|
||||
- Documentación de uso de WebDAV con OxiCloud
|
||||
- Compatibilidad con los clientes WebDAV más comunes
|
||||
|
||||
### Fase 3: CalDAV (Estimado: 4-5 semanas)
|
||||
|
||||
**Objetivos:**
|
||||
- Implementar el protocolo CalDAV (RFC4791)
|
||||
- Crear entidades y repositorios para calendarios y eventos
|
||||
- Soportar operaciones de calendario con clientes comunes
|
||||
|
||||
**Tareas:**
|
||||
1. **Semana 1: Modelo de Datos**
|
||||
- Implementar entidades Calendar y CalendarEvent
|
||||
- Desarrollar repositorios para almacenamiento de datos
|
||||
- Crear DTOs y adaptadores CalDAV
|
||||
|
||||
2. **Semana 2: Endpoints Básicos**
|
||||
- Implementar PROPFIND para detección de calendarios
|
||||
- Desarrollar MKCALENDAR para creación de calendarios
|
||||
- Implementar GET/PUT para eventos individuales
|
||||
|
||||
3. **Semana 3: Consultas Avanzadas**
|
||||
- Implementar REPORT para consultas de calendario
|
||||
- Desarrollar soporte para búsqueda por rango de fechas
|
||||
- Añadir manejo de recurrencias (reglas RRULE)
|
||||
|
||||
4. **Semana 4: Interoperabilidad**
|
||||
- Implementar sincronización eficiente (collection-sync)
|
||||
- Añadir soporte para zonas horarias
|
||||
- Desarrollar manejo de alarmas y notificaciones
|
||||
|
||||
5. **Semana 5: Pruebas y Refinamiento**
|
||||
- Probar con clientes CalDAV populares
|
||||
- Optimizar rendimiento para calendarios grandes
|
||||
- Documentar APIs y comportamiento CalDAV
|
||||
|
||||
**Entregables:**
|
||||
- Implementación completa de CalDAV (RFC4791)
|
||||
- Soporte para creación y gestión de calendarios
|
||||
- Compatibilidad con clientes CalDAV populares
|
||||
- Documentación de uso de CalDAV con OxiCloud
|
||||
|
||||
### Fase 4: CardDAV (Estimado: 3-4 semanas)
|
||||
|
||||
**Objetivos:**
|
||||
- Implementar el protocolo CardDAV (RFC6352)
|
||||
- Crear entidades y repositorios para libretas de direcciones y contactos
|
||||
- Soportar operaciones de contactos con clientes comunes
|
||||
|
||||
**Tareas:**
|
||||
1. **Semana 1: Modelo de Datos**
|
||||
- Implementar entidades AddressBook y Contact
|
||||
- Desarrollar repositorios para almacenamiento de datos
|
||||
- Crear DTOs y adaptadores CardDAV
|
||||
|
||||
2. **Semana 2: Endpoints Básicos**
|
||||
- Implementar PROPFIND para detección de libretas
|
||||
- Desarrollar MKCOL para creación de libretas de direcciones
|
||||
- Implementar GET/PUT para contactos individuales
|
||||
|
||||
3. **Semana 3: Consultas y Búsqueda**
|
||||
- Implementar REPORT para consultas de contactos
|
||||
- Desarrollar búsqueda de contactos por criterios
|
||||
- Añadir soporte para grupos de contactos
|
||||
|
||||
4. **Semana 4: Pruebas y Refinamiento**
|
||||
- Probar con clientes CardDAV populares
|
||||
- Optimizar rendimiento para libretas grandes
|
||||
- Documentar APIs y comportamiento CardDAV
|
||||
|
||||
**Entregables:**
|
||||
- Implementación completa de CardDAV (RFC6352)
|
||||
- Soporte para creación y gestión de libretas de direcciones
|
||||
- Compatibilidad con clientes CardDAV populares
|
||||
- Documentación de uso de CardDAV con OxiCloud
|
||||
|
||||
### Fase 5: Integración y Lanzamiento (Estimado: 2-3 semanas)
|
||||
|
||||
**Objetivos:**
|
||||
- Integrar todos los protocolos DAV en una solución cohesiva
|
||||
- Asegurar compatibilidad cruzada entre protocolos
|
||||
- Preparar la documentación y materiales para lanzamiento
|
||||
|
||||
**Tareas:**
|
||||
1. **Semana 1: Integración**
|
||||
- Consolidar código compartido entre protocolos
|
||||
- Asegurar coherencia de comportamiento
|
||||
- Refinar manejo de errores y recuperación
|
||||
|
||||
2. **Semana 2: Pruebas de Sistema**
|
||||
- Realizar pruebas de integración end-to-end
|
||||
- Validar rendimiento bajo carga
|
||||
- Verificar seguridad y permisos
|
||||
|
||||
3. **Semana 3: Documentación y Lanzamiento**
|
||||
- Finalizar guías de usuario para clientes DAV
|
||||
- Crear documentación para desarrolladores
|
||||
- Preparar materiales de lanzamiento
|
||||
|
||||
**Entregables:**
|
||||
- Solución DAV completa e integrada
|
||||
- Documentación comprensiva para usuarios y desarrolladores
|
||||
- Paquete de lanzamiento listo para despliegue
|
||||
|
||||
## Requisitos de Infraestructura
|
||||
|
||||
### Dependencias de Bibliotecas
|
||||
|
||||
```toml
|
||||
# Añadir a Cargo.toml
|
||||
[dependencies]
|
||||
# Procesamiento XML
|
||||
quick-xml = "0.30.0"
|
||||
xml-rs = "0.8.14"
|
||||
|
||||
# Soporte para iCalendar
|
||||
icalendar = "0.15.0"
|
||||
|
||||
# Soporte para vCard
|
||||
vcard = "0.2.0"
|
||||
|
||||
# Utilidades para DAV
|
||||
http-multipart = "0.3.0"
|
||||
```
|
||||
|
||||
### Esquema de Base de Datos
|
||||
|
||||
Las nuevas tablas para CalDAV y CardDAV deben ser creadas como parte de la fase correspondiente. Ver el esquema completo en el documento principal de implementación.
|
||||
|
||||
## Estrategia de Pruebas
|
||||
|
||||
### Pruebas Unitarias
|
||||
|
||||
- Pruebas de serialización/deserialización XML
|
||||
- Pruebas de validación de entradas
|
||||
- Pruebas de lógica de negocio para cada operación DAV
|
||||
|
||||
### Pruebas de Integración
|
||||
|
||||
- Pruebas end-to-end con clientes simulados
|
||||
- Pruebas de flujos completos (creación, actualización, eliminación)
|
||||
- Pruebas de concurrencia y manejo de conflictos
|
||||
|
||||
### Pruebas de Compatibilidad
|
||||
|
||||
- Matriz de pruebas con clientes reales (al menos 3 por protocolo)
|
||||
- Pruebas en diferentes sistemas operativos
|
||||
- Verificación de conformidad con RFCs
|
||||
|
||||
## Consideraciones de Rendimiento
|
||||
|
||||
1. **Optimización de Consultas**
|
||||
- Implementar paginación para conjuntos grandes de resultados
|
||||
- Optimizar consultas SQL para calendarios y contactos
|
||||
- Utilizar índices adecuados para búsqueda rápida
|
||||
|
||||
2. **Caché**
|
||||
- Implementar caché de propiedades para respuestas PROPFIND
|
||||
- Usar ETags para validación de caché
|
||||
- Aplicar caché de consultas para reportes frecuentes
|
||||
|
||||
3. **Procesamiento Eficiente**
|
||||
- Procesamiento XML eficiente para solicitudes grandes
|
||||
- Streaming de datos para archivos grandes
|
||||
- Procesamiento asíncrono para operaciones costosas
|
||||
|
||||
## Riesgos y Mitigación
|
||||
|
||||
| Riesgo | Impacto | Probabilidad | Estrategia de Mitigación |
|
||||
|--------|---------|--------------|--------------------------|
|
||||
| Problemas de compatibilidad con clientes | Alto | Medio | Pruebas tempranas con variedad de clientes, seguir estrictamente las especificaciones |
|
||||
| Rendimiento insuficiente | Medio | Bajo | Pruebas de carga desde el inicio, diseño para escalabilidad |
|
||||
| Complejidad excesiva | Medio | Medio | Enfoque modular, abstracciones claras, revisiones de código frecuentes |
|
||||
| Problemas de seguridad | Alto | Bajo | Revisiones de seguridad, validación estricta de entradas, pruebas de penetración |
|
||||
| Retrasos en el cronograma | Medio | Medio | Planificación conservadora, hitos claros, enfoque iterativo |
|
||||
|
||||
## Criterios de Éxito
|
||||
|
||||
1. **Compatibilidad**
|
||||
- Todos los protocolos cumplen con sus respectivos RFCs
|
||||
- Compatibilidad verificada con al menos 3 clientes principales por protocolo
|
||||
- Funciona en todos los sistemas operativos principales
|
||||
|
||||
2. **Rendimiento**
|
||||
- Tiempo de respuesta para operaciones típicas < 500ms
|
||||
- Soporta calendarios con >1000 eventos sin degradación significativa
|
||||
- Soporta libretas con >1000 contactos sin degradación significativa
|
||||
|
||||
3. **Usabilidad**
|
||||
- Proceso de configuración de cliente sencillo y documentado
|
||||
- Mensajes de error claros y específicos
|
||||
- Documentación completa para usuarios y desarrolladores
|
||||
|
||||
## Recursos Necesarios
|
||||
|
||||
1. **Equipo de Desarrollo**
|
||||
- 1-2 desarrolladores de backend (Rust)
|
||||
- 1 desarrollador de frontend (para integración UI si es necesario)
|
||||
- 1 tester
|
||||
|
||||
2. **Infraestructura**
|
||||
- Entorno de pruebas con múltiples sistemas operativos
|
||||
- Clientes DAV variados para pruebas
|
||||
- Servidor de CI/CD para pruebas automatizadas
|
||||
|
||||
3. **Habilidades**
|
||||
- Experiencia con protocolos HTTP avanzados
|
||||
- Conocimiento de procesamiento XML
|
||||
- Familiaridad con los estándares WebDAV, CalDAV y CardDAV
|
||||
|
||||
## Próximos Pasos
|
||||
|
||||
1. Asignar recursos al proyecto
|
||||
2. Establecer repositorio de código y estructura inicial
|
||||
3. Iniciar la Fase 1 (Infraestructura DAV Común)
|
||||
4. Configurar entorno de CI/CD para pruebas
|
||||
5. Revisar y refinar el plan según sea necesario durante la implementación
|
||||
@@ -0,0 +1,603 @@
|
||||
# Integración de WebDAV, CalDAV y CardDAV en OxiCloud
|
||||
|
||||
Este documento describe el diseño e implementación de los protocolos WebDAV, CalDAV y CardDAV en OxiCloud, extendiendo la plataforma para soportar clientes y dispositivos que utilizan estos estándares.
|
||||
|
||||
## Tabla de Contenidos
|
||||
|
||||
1. [Introducción](#introducción)
|
||||
2. [Arquitectura de la Implementación](#arquitectura-de-la-implementación)
|
||||
3. [WebDAV](#webdav)
|
||||
4. [CalDAV](#caldav)
|
||||
5. [CardDAV](#carddav)
|
||||
6. [Consideraciones de Seguridad](#consideraciones-de-seguridad)
|
||||
7. [Pruebas y Compatibilidad](#pruebas-y-compatibilidad)
|
||||
|
||||
## Introducción
|
||||
|
||||
### WebDAV (Web Distributed Authoring and Versioning)
|
||||
WebDAV es una extensión del protocolo HTTP que permite a los clientes realizar operaciones sobre archivos en un servidor remoto, como crear, modificar, mover y eliminar archivos y directorios.
|
||||
|
||||
### CalDAV (Calendaring Extensions to WebDAV)
|
||||
CalDAV es un protocolo basado en WebDAV que permite a los clientes acceder y gestionar datos de calendario, como eventos y tareas.
|
||||
|
||||
### CardDAV (vCard Extensions to WebDAV)
|
||||
CardDAV es un protocolo que extiende WebDAV para permitir el acceso y gestión de datos de contactos en formato vCard.
|
||||
|
||||
## Arquitectura de la Implementación
|
||||
|
||||
La implementación de los protocolos DAV se integra en la arquitectura hexagonal existente de OxiCloud:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ INTERFACES │
|
||||
│ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────────────┐ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ REST API │ │ WebDAV API │ │ CalDAV/CardDAV API │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ └───────┬───────┘ └───────┬───────┘ └───────────┬───────────┘ │
|
||||
│ │ │ │ │
|
||||
└──────────┼──────────────────┼──────────────────────┼──────────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ APLICACIÓN │
|
||||
│ │
|
||||
│ ┌───────────┐ ┌────────────┐ ┌───────────┐ ┌──────────────┐ │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ │FileService│ │FolderService│ │CalService │ │ContactService│ │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ └─────┬─────┘ └──────┬─────┘ └─────┬─────┘ └──────┬───────┘ │
|
||||
│ │ │ │ │ │
|
||||
└────────┼───────────────┼──────────────┼───────────────┼─────────┘
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ DOMINIO │
|
||||
│ │
|
||||
│ ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌───────────────┐ │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ │ File │ │ Folder │ │ Calendar │ │ Contact │ │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ └─────────┘ └──────────┘ └────────────┘ └───────────────┘ │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Componentes Principales
|
||||
|
||||
1. **Adaptadores DAV**: Convertirán entre las especificaciones DAV y los modelos de OxiCloud
|
||||
2. **Servicios de Aplicación**: Se extenderán para incluir funcionalidades específicas DAV
|
||||
3. **Modelos de Dominio**: Se añadirán nuevas entidades para Calendar y Contact
|
||||
4. **Repositorios**: Implementaciones de almacenamiento para calendarios y contactos
|
||||
|
||||
## WebDAV
|
||||
|
||||
### Endpoints Requeridos
|
||||
|
||||
| Método HTTP | Endpoint | Descripción |
|
||||
|-------------|----------|-------------|
|
||||
| OPTIONS | /webdav/{path} | Indica las capacidades WebDAV soportadas |
|
||||
| PROPFIND | /webdav/{path} | Recupera propiedades de recursos |
|
||||
| PROPPATCH | /webdav/{path} | Modifica propiedades de recursos |
|
||||
| MKCOL | /webdav/{path} | Crea colecciones (directorios) |
|
||||
| GET | /webdav/{path} | Recupera contenido de recursos |
|
||||
| HEAD | /webdav/{path} | Recupera metadatos de recursos |
|
||||
| PUT | /webdav/{path} | Crea o actualiza recursos |
|
||||
| DELETE | /webdav/{path} | Elimina recursos |
|
||||
| COPY | /webdav/{path} | Copia recursos |
|
||||
| MOVE | /webdav/{path} | Mueve recursos |
|
||||
| LOCK | /webdav/{path} | Bloquea recursos |
|
||||
| UNLOCK | /webdav/{path} | Desbloquea recursos |
|
||||
|
||||
### Implementación
|
||||
|
||||
1. **Manejador WebDAV**:
|
||||
|
||||
```rust
|
||||
// src/interfaces/api/handlers/webdav_handler.rs
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
Router,
|
||||
routing::get,
|
||||
extract::{Path, State, Request, Extension},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
use http::{Method, header};
|
||||
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::application::ports::file_ports::{FileRetrievalUseCase, FileUploadUseCase};
|
||||
use crate::application::ports::folder_ports::FolderUseCase;
|
||||
use crate::common::errors::AppError;
|
||||
|
||||
pub fn webdav_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/webdav/*path", get(handle_get))
|
||||
.route_with_tsr("/webdav/*path", axum::routing::on(
|
||||
Method::OPTIONS, handle_options,
|
||||
Method::PROPFIND, handle_propfind,
|
||||
Method::PROPPATCH, handle_proppatch,
|
||||
Method::MKCOL, handle_mkcol,
|
||||
Method::PUT, handle_put,
|
||||
Method::DELETE, handle_delete,
|
||||
Method::COPY, handle_copy,
|
||||
Method::MOVE, handle_move,
|
||||
Method::LOCK, handle_lock,
|
||||
Method::UNLOCK, handle_unlock,
|
||||
))
|
||||
}
|
||||
|
||||
// Implementar funciones para cada método WebDAV...
|
||||
```
|
||||
|
||||
2. **Adaptador WebDAV**:
|
||||
|
||||
```rust
|
||||
// src/application/adapters/webdav_adapter.rs
|
||||
use xml::reader::{EventReader, XmlEvent};
|
||||
use xml::writer::{EventWriter, EmitterConfig, XmlEvent as WriteEvent};
|
||||
use std::io::{Read, Write};
|
||||
use crate::application::dtos::file_dto::FileDto;
|
||||
use crate::application::dtos::folder_dto::FolderDto;
|
||||
|
||||
/// Convierte entre objetos de OxiCloud y representaciones WebDAV
|
||||
pub struct WebDavAdapter;
|
||||
|
||||
impl WebDavAdapter {
|
||||
/// Convierte una propiedad PROPFIND en XML a un objeto de solicitud
|
||||
pub fn parse_propfind<R: Read>(reader: R) -> Result<PropFindRequest, Error> {
|
||||
// Implementación...
|
||||
}
|
||||
|
||||
/// Genera respuesta XML para PROPFIND basada en archivos y carpetas
|
||||
pub fn generate_propfind_response<W: Write>(
|
||||
writer: W,
|
||||
files: &[FileDto],
|
||||
folders: &[FolderDto],
|
||||
base_url: &str,
|
||||
) -> Result<(), Error> {
|
||||
// Implementación...
|
||||
}
|
||||
|
||||
// Otros métodos para manejar diferentes operaciones WebDAV...
|
||||
}
|
||||
```
|
||||
|
||||
## CalDAV
|
||||
|
||||
### Endpoints Requeridos
|
||||
|
||||
| Método HTTP | Endpoint | Descripción |
|
||||
|-------------|----------|-------------|
|
||||
| PROPFIND | /caldav/{calendar} | Recupera propiedades del calendario |
|
||||
| REPORT | /caldav/{calendar} | Consulta eventos del calendario |
|
||||
| MKCALENDAR | /caldav/{calendar} | Crea un nuevo calendario |
|
||||
| PUT | /caldav/{calendar}/{event}.ics | Crea o actualiza un evento |
|
||||
| GET | /caldav/{calendar}/{event}.ics | Recupera un evento |
|
||||
| DELETE | /caldav/{calendar}/{event}.ics | Elimina un evento |
|
||||
|
||||
### Implementación
|
||||
|
||||
1. **Nuevas Entidades de Dominio**:
|
||||
|
||||
```rust
|
||||
// src/domain/entities/calendar.rs
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Calendar {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
owner_id: String,
|
||||
description: Option<String>,
|
||||
color: Option<String>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// src/domain/entities/calendar_event.rs
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CalendarEvent {
|
||||
id: Uuid,
|
||||
calendar_id: Uuid,
|
||||
summary: String,
|
||||
description: Option<String>,
|
||||
location: Option<String>,
|
||||
start_time: DateTime<Utc>,
|
||||
end_time: DateTime<Utc>,
|
||||
all_day: bool,
|
||||
rrule: Option<String>, // Regla de recurrencia
|
||||
ical_data: String, // Datos iCalendar completos
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
2. **Repositorios**:
|
||||
|
||||
```rust
|
||||
// src/domain/repositories/calendar_repository.rs
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use crate::domain::entities::calendar::Calendar;
|
||||
use crate::common::errors::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait CalendarRepository: Send + Sync {
|
||||
async fn create_calendar(&self, calendar: Calendar) -> Result<Calendar>;
|
||||
async fn get_calendar_by_id(&self, id: &Uuid) -> Result<Calendar>;
|
||||
async fn get_calendars_by_owner(&self, owner_id: &str) -> Result<Vec<Calendar>>;
|
||||
async fn update_calendar(&self, calendar: Calendar) -> Result<Calendar>;
|
||||
async fn delete_calendar(&self, id: &Uuid) -> Result<()>;
|
||||
}
|
||||
|
||||
// src/domain/repositories/calendar_event_repository.rs
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::domain::entities::calendar_event::CalendarEvent;
|
||||
use crate::common::errors::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait CalendarEventRepository: Send + Sync {
|
||||
async fn create_event(&self, event: CalendarEvent) -> Result<CalendarEvent>;
|
||||
async fn get_event_by_id(&self, id: &Uuid) -> Result<CalendarEvent>;
|
||||
async fn get_events_by_calendar(&self, calendar_id: &Uuid) -> Result<Vec<CalendarEvent>>;
|
||||
async fn get_events_in_timerange(
|
||||
&self,
|
||||
calendar_id: &Uuid,
|
||||
start: &DateTime<Utc>,
|
||||
end: &DateTime<Utc>
|
||||
) -> Result<Vec<CalendarEvent>>;
|
||||
async fn update_event(&self, event: CalendarEvent) -> Result<CalendarEvent>;
|
||||
async fn delete_event(&self, id: &Uuid) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
3. **Servicio CalDAV**:
|
||||
|
||||
```rust
|
||||
// src/application/services/caldav_service.rs
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
use crate::domain::repositories::calendar_repository::CalendarRepository;
|
||||
use crate::domain::repositories::calendar_event_repository::CalendarEventRepository;
|
||||
use crate::domain::entities::calendar::Calendar;
|
||||
use crate::domain::entities::calendar_event::CalendarEvent;
|
||||
use crate::application::dtos::calendar_dto::{CalendarDto, CalendarEventDto};
|
||||
use crate::common::errors::{Result, DomainError};
|
||||
|
||||
pub struct CalDavService {
|
||||
calendar_repository: Arc<dyn CalendarRepository>,
|
||||
event_repository: Arc<dyn CalendarEventRepository>,
|
||||
}
|
||||
|
||||
impl CalDavService {
|
||||
pub fn new(
|
||||
calendar_repository: Arc<dyn CalendarRepository>,
|
||||
event_repository: Arc<dyn CalendarEventRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
calendar_repository,
|
||||
event_repository,
|
||||
}
|
||||
}
|
||||
|
||||
// Implementar métodos para operaciones CalDAV...
|
||||
}
|
||||
```
|
||||
|
||||
4. **Manejador CalDAV**:
|
||||
|
||||
```rust
|
||||
// src/interfaces/api/handlers/caldav_handler.rs
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, put, delete},
|
||||
extract::{Path, State, Request, Extension},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
use http::{Method, header};
|
||||
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::application::services::caldav_service::CalDavService;
|
||||
use crate::common::errors::AppError;
|
||||
|
||||
pub fn caldav_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/caldav/", get(get_calendars))
|
||||
.route("/caldav/:calendar", get(get_calendar))
|
||||
.route_with_tsr("/caldav/:calendar", axum::routing::on(
|
||||
Method::PROPFIND, handle_calendar_propfind,
|
||||
Method::REPORT, handle_calendar_report,
|
||||
Method::MKCALENDAR, handle_mkcalendar,
|
||||
))
|
||||
.route("/caldav/:calendar/:event", get(get_event))
|
||||
.route("/caldav/:calendar/:event", put(put_event))
|
||||
.route("/caldav/:calendar/:event", delete(delete_event))
|
||||
}
|
||||
|
||||
// Implementar funciones para cada método CalDAV...
|
||||
```
|
||||
|
||||
## CardDAV
|
||||
|
||||
### Endpoints Requeridos
|
||||
|
||||
| Método HTTP | Endpoint | Descripción |
|
||||
|-------------|----------|-------------|
|
||||
| PROPFIND | /carddav/addressbooks/{addressbook} | Recupera propiedades de la libreta de direcciones |
|
||||
| REPORT | /carddav/addressbooks/{addressbook} | Consulta contactos |
|
||||
| MKCOL | /carddav/addressbooks/{addressbook} | Crea una nueva libreta de direcciones |
|
||||
| PUT | /carddav/addressbooks/{addressbook}/{contact}.vcf | Crea o actualiza un contacto |
|
||||
| GET | /carddav/addressbooks/{addressbook}/{contact}.vcf | Recupera un contacto |
|
||||
| DELETE | /carddav/addressbooks/{addressbook}/{contact}.vcf | Elimina un contacto |
|
||||
|
||||
### Implementación
|
||||
|
||||
1. **Nuevas Entidades de Dominio**:
|
||||
|
||||
```rust
|
||||
// src/domain/entities/address_book.rs
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AddressBook {
|
||||
id: Uuid,
|
||||
name: String,
|
||||
owner_id: String,
|
||||
description: Option<String>,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
// src/domain/entities/contact.rs
|
||||
use uuid::Uuid;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Contact {
|
||||
id: Uuid,
|
||||
address_book_id: Uuid,
|
||||
full_name: String,
|
||||
first_name: Option<String>,
|
||||
last_name: Option<String>,
|
||||
email: Option<String>,
|
||||
phone: Option<String>,
|
||||
address: Option<String>,
|
||||
organization: Option<String>,
|
||||
vcard_data: String, // Datos vCard completos
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
}
|
||||
```
|
||||
|
||||
2. **Repositorios**:
|
||||
|
||||
```rust
|
||||
// src/domain/repositories/address_book_repository.rs
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use crate::domain::entities::address_book::AddressBook;
|
||||
use crate::common::errors::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait AddressBookRepository: Send + Sync {
|
||||
async fn create_address_book(&self, address_book: AddressBook) -> Result<AddressBook>;
|
||||
async fn get_address_book_by_id(&self, id: &Uuid) -> Result<AddressBook>;
|
||||
async fn get_address_books_by_owner(&self, owner_id: &str) -> Result<Vec<AddressBook>>;
|
||||
async fn update_address_book(&self, address_book: AddressBook) -> Result<AddressBook>;
|
||||
async fn delete_address_book(&self, id: &Uuid) -> Result<()>;
|
||||
}
|
||||
|
||||
// src/domain/repositories/contact_repository.rs
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
use crate::domain::entities::contact::Contact;
|
||||
use crate::common::errors::Result;
|
||||
|
||||
#[async_trait]
|
||||
pub trait ContactRepository: Send + Sync {
|
||||
async fn create_contact(&self, contact: Contact) -> Result<Contact>;
|
||||
async fn get_contact_by_id(&self, id: &Uuid) -> Result<Contact>;
|
||||
async fn get_contacts_by_address_book(&self, address_book_id: &Uuid) -> Result<Vec<Contact>>;
|
||||
async fn search_contacts(&self, address_book_id: &Uuid, query: &str) -> Result<Vec<Contact>>;
|
||||
async fn update_contact(&self, contact: Contact) -> Result<Contact>;
|
||||
async fn delete_contact(&self, id: &Uuid) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
3. **Servicio CardDAV**:
|
||||
|
||||
```rust
|
||||
// src/application/services/carddav_service.rs
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
use crate::domain::repositories::address_book_repository::AddressBookRepository;
|
||||
use crate::domain::repositories::contact_repository::ContactRepository;
|
||||
use crate::domain::entities::address_book::AddressBook;
|
||||
use crate::domain::entities::contact::Contact;
|
||||
use crate::application::dtos::address_book_dto::{AddressBookDto, ContactDto};
|
||||
use crate::common::errors::{Result, DomainError};
|
||||
|
||||
pub struct CardDavService {
|
||||
address_book_repository: Arc<dyn AddressBookRepository>,
|
||||
contact_repository: Arc<dyn ContactRepository>,
|
||||
}
|
||||
|
||||
impl CardDavService {
|
||||
pub fn new(
|
||||
address_book_repository: Arc<dyn AddressBookRepository>,
|
||||
contact_repository: Arc<dyn ContactRepository>,
|
||||
) -> Self {
|
||||
Self {
|
||||
address_book_repository,
|
||||
contact_repository,
|
||||
}
|
||||
}
|
||||
|
||||
// Implementar métodos para operaciones CardDAV...
|
||||
}
|
||||
```
|
||||
|
||||
4. **Manejador CardDAV**:
|
||||
|
||||
```rust
|
||||
// src/interfaces/api/handlers/carddav_handler.rs
|
||||
use std::sync::Arc;
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{get, put, delete},
|
||||
extract::{Path, State, Request, Extension},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
use http::{Method, header};
|
||||
|
||||
use crate::common::di::AppState;
|
||||
use crate::interfaces::middleware::auth::CurrentUser;
|
||||
use crate::application::services::carddav_service::CardDavService;
|
||||
use crate::common::errors::AppError;
|
||||
|
||||
pub fn carddav_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/carddav/addressbooks/", get(get_address_books))
|
||||
.route("/carddav/addressbooks/:addressbook", get(get_address_book))
|
||||
.route_with_tsr("/carddav/addressbooks/:addressbook", axum::routing::on(
|
||||
Method::PROPFIND, handle_addressbook_propfind,
|
||||
Method::REPORT, handle_addressbook_report,
|
||||
Method::MKCOL, handle_mkaddressbook,
|
||||
))
|
||||
.route("/carddav/addressbooks/:addressbook/:contact", get(get_contact))
|
||||
.route("/carddav/addressbooks/:addressbook/:contact", put(put_contact))
|
||||
.route("/carddav/addressbooks/:addressbook/:contact", delete(delete_contact))
|
||||
}
|
||||
|
||||
// Implementar funciones para cada método CardDAV...
|
||||
```
|
||||
|
||||
## Esquema de Base de Datos
|
||||
|
||||
```sql
|
||||
-- Esquema para CalDAV
|
||||
CREATE TABLE calendar (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
owner_id VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
color VARCHAR(50),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE calendar_event (
|
||||
id UUID PRIMARY KEY,
|
||||
calendar_id UUID NOT NULL REFERENCES calendar(id) ON DELETE CASCADE,
|
||||
summary VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
location TEXT,
|
||||
start_time TIMESTAMPTZ NOT NULL,
|
||||
end_time TIMESTAMPTZ NOT NULL,
|
||||
all_day BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
rrule TEXT,
|
||||
ical_uid VARCHAR(255) NOT NULL,
|
||||
ical_data TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Esquema para CardDAV
|
||||
CREATE TABLE address_book (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
owner_id VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE contact (
|
||||
id UUID PRIMARY KEY,
|
||||
address_book_id UUID NOT NULL REFERENCES address_book(id) ON DELETE CASCADE,
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
first_name VARCHAR(255),
|
||||
last_name VARCHAR(255),
|
||||
email VARCHAR(255),
|
||||
phone VARCHAR(100),
|
||||
address TEXT,
|
||||
organization VARCHAR(255),
|
||||
vcard_uid VARCHAR(255) NOT NULL,
|
||||
vcard_data TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Índices para búsqueda eficiente
|
||||
CREATE INDEX idx_calendar_owner ON calendar(owner_id);
|
||||
CREATE INDEX idx_calendar_event_calendar ON calendar_event(calendar_id);
|
||||
CREATE INDEX idx_address_book_owner ON address_book(owner_id);
|
||||
CREATE INDEX idx_contact_address_book ON contact(address_book_id);
|
||||
CREATE INDEX idx_contact_name ON contact(full_name);
|
||||
```
|
||||
|
||||
## Consideraciones de Seguridad
|
||||
|
||||
1. **Autenticación**
|
||||
- Utilizar la autenticación existente de OxiCloud
|
||||
- Soportar autenticación HTTP Basic para clientes DAV
|
||||
- Implementar el esquema de autenticación Digest si es necesario
|
||||
|
||||
2. **Autorización**
|
||||
- Verificar permisos de usuario para acceder a recursos
|
||||
- Implementar control de acceso basado en propietario y permisos compartidos
|
||||
- Asegurar que los usuarios solo puedan acceder a sus propios calendarios y libretas de direcciones
|
||||
|
||||
3. **Prevención de Ataques**
|
||||
- Validar y sanitizar todas las entradas XML
|
||||
- Limitar tamaño máximo de carga útil
|
||||
- Implementar rate limiting en endpoints DAV
|
||||
|
||||
## Pruebas y Compatibilidad
|
||||
|
||||
### Clientes a Probar
|
||||
|
||||
1. **WebDAV**
|
||||
- Windows Explorer
|
||||
- macOS Finder
|
||||
- Cyberduck
|
||||
- FileZilla (con extensión WebDAV)
|
||||
|
||||
2. **CalDAV**
|
||||
- Apple Calendar
|
||||
- Mozilla Thunderbird (Lightning)
|
||||
- Microsoft Outlook (con complemento CalDAV)
|
||||
- Google Calendar (mediante sincronización)
|
||||
|
||||
3. **CardDAV**
|
||||
- Apple Contacts
|
||||
- Mozilla Thunderbird
|
||||
- Microsoft Outlook (con complemento CardDAV)
|
||||
- Google Contacts (mediante sincronización)
|
||||
|
||||
### Pruebas de Cumplimiento
|
||||
|
||||
- Utilizar la suite de pruebas CalDAVTester para verificar la conformidad con el estándar
|
||||
- Validar cumplimiento de RFC para cada protocolo
|
||||
- Pruebas de stress para evaluar rendimiento bajo carga
|
||||
|
||||
### Depuración
|
||||
|
||||
- Implementar logging detallado para operaciones DAV
|
||||
- Crear herramientas de diagnóstico para depurar solicitudes DAV complejas
|
||||
- Proporcionar mensajes de error claros para ayudar en la resolución de problemas
|
||||
@@ -0,0 +1,162 @@
|
||||
# Arquitectura de Integración OIDC en OxiCloud
|
||||
|
||||
Este documento describe la arquitectura y el flujo de autenticación OpenID Connect (OIDC) en OxiCloud.
|
||||
|
||||
## Diagrama de Arquitectura
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ PROVEEDOR DE IDENTIDAD │
|
||||
│ │
|
||||
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ Authentik │ │ Authelia │ │ KeyCloak │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
|
||||
│ │ │ │ │
|
||||
└────────────┼──────────────────────┼──────────────────────┼─────────────┘
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ │ │
|
||||
│ OIDC │
|
||||
│ │ │
|
||||
│ │ │
|
||||
┌────────────┼──────────────────────┼──────────────────────┼─────────────┐
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ OXICLOUD │ │
|
||||
│ │ │ │
|
||||
│ │ ┌───────────────┐ ┌───────────────┐ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ OidcService │◄────►│ AuthService │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ └───────┬───────┘ └───────┬───────┘ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ▼ ▼ │ │
|
||||
│ │ ┌───────────────────────────────────────────┐ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ AuthApplicationService │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ └───────────────────┬───────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ ┌───────────────────────────────────────────┐ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ Auth Handler │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ └───────────────────────────────────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│
|
||||
│ HTTP/HTTPS
|
||||
│
|
||||
│
|
||||
┌────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
│ NAVEGADOR WEB │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ Interfaz de Usuario │ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ Login.html │ │ oidcAuth.js │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ └──────────────┘ └──────────────┘ │ │
|
||||
│ │ │ │
|
||||
│ └───────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Flujo de Autenticación OIDC
|
||||
|
||||
El flujo de autenticación OIDC en OxiCloud sigue el flujo de código de autorización (Authorization Code Flow):
|
||||
|
||||
1. **Inicio de la Autenticación**:
|
||||
- El usuario hace clic en "Login con [Proveedor]" en la página de inicio de sesión.
|
||||
- El frontend genera un estado aleatorio para protección CSRF.
|
||||
- El frontend solicita a OxiCloud una URL de autorización.
|
||||
|
||||
2. **Redirección al Proveedor de Identidad**:
|
||||
- OxiCloud genera una URL de autorización y la devuelve al frontend.
|
||||
- El navegador redirige al usuario a la página de inicio de sesión del proveedor de identidad.
|
||||
|
||||
3. **Autenticación en el Proveedor**:
|
||||
- El usuario se autentica en el proveedor de identidad (con contraseña, 2FA, etc.).
|
||||
- El proveedor redirige al usuario de vuelta a OxiCloud con un código de autorización.
|
||||
|
||||
4. **Intercambio del Código de Autorización**:
|
||||
- El frontend de OxiCloud recibe el código de autorización y lo envía al backend.
|
||||
- OxiCloud intercambia el código por tokens de acceso e ID con el proveedor de identidad.
|
||||
- OxiCloud verifica el token de ID y extrae la información del usuario.
|
||||
|
||||
5. **Creación/Recuperación de Usuario**:
|
||||
- OxiCloud busca un usuario existente con el ID externo del proveedor.
|
||||
- Si no existe y la creación automática está habilitada, se crea un nuevo usuario.
|
||||
- Si no existe y la creación automática está deshabilitada, se devuelve un error.
|
||||
|
||||
6. **Generación de Tokens de Sesión**:
|
||||
- OxiCloud genera sus propios tokens de acceso y actualización para el usuario.
|
||||
- Estos tokens se utilizan para autenticar las solicitudes subsiguientes a la API de OxiCloud.
|
||||
|
||||
7. **Respuesta al Cliente**:
|
||||
- OxiCloud devuelve los tokens y la información del usuario al frontend.
|
||||
- El frontend almacena los tokens y redirige al usuario a la página principal.
|
||||
|
||||
## Componentes Principales
|
||||
|
||||
### 1. OidcService
|
||||
|
||||
Este servicio gestiona la comunicación con los proveedores OIDC:
|
||||
- Descubre los endpoints OIDC de los proveedores
|
||||
- Genera URLs de autorización
|
||||
- Intercambia códigos de autorización por tokens
|
||||
- Verifica tokens y extrae información de usuario
|
||||
|
||||
### 2. AuthApplicationService
|
||||
|
||||
Coordina el proceso de autenticación:
|
||||
- Proporciona una interfaz entre la capa de API y los servicios de dominio
|
||||
- Gestiona el proceso de creación/recuperación de usuarios
|
||||
- Coordina la generación de tokens de acceso para OxiCloud
|
||||
|
||||
### 3. Auth Handler
|
||||
|
||||
Expone endpoints HTTP para el flujo de autenticación OIDC:
|
||||
- `/api/auth/oidc/providers` - Lista los proveedores OIDC disponibles
|
||||
- `/api/auth/oidc/auth` - Genera una URL de autorización para un proveedor
|
||||
- `/api/auth/oidc/callback` - Procesa la respuesta del proveedor y completa la autenticación
|
||||
|
||||
### 4. Frontend (oidcAuth.js)
|
||||
|
||||
Gestiona la parte del cliente del flujo de autenticación:
|
||||
- Muestra botones para los proveedores OIDC
|
||||
- Inicia el flujo de autenticación
|
||||
- Maneja la redirección de retorno del proveedor
|
||||
- Procesa y almacena los tokens de sesión
|
||||
|
||||
## Configuración Multi-Proveedor
|
||||
|
||||
OxiCloud permite configurar múltiples proveedores OIDC simultáneamente:
|
||||
|
||||
1. **Configuración Separada**: Cada proveedor tiene su propia configuración independiente.
|
||||
2. **Selección de Proveedor**: Los usuarios pueden elegir con qué proveedor autenticarse.
|
||||
3. **Mapeo de Identidades**: OxiCloud mapea identidades de diferentes proveedores a usuarios internos.
|
||||
|
||||
## Seguridad
|
||||
|
||||
La implementación OIDC en OxiCloud incluye varias medidas de seguridad:
|
||||
|
||||
1. **Protección CSRF**: Utiliza un estado aleatorio para prevenir ataques CSRF.
|
||||
2. **Validación de Tokens**: Verifica firmas y vigencia de los tokens JWT.
|
||||
3. **Código de Autorización**: Utiliza el flujo de código de autorización, que es más seguro que el flujo implícito.
|
||||
4. **HTTPS**: Requiere conexiones HTTPS para todas las comunicaciones OIDC.
|
||||
5. **Secretos del Cliente**: Los secretos del cliente se almacenan de forma segura y nunca se exponen al frontend.
|
||||
@@ -0,0 +1,218 @@
|
||||
# Ejemplos de Configuración de OIDC para OxiCloud
|
||||
|
||||
Esta guía proporciona ejemplos de configuración para integrar OxiCloud con diferentes proveedores OIDC (OpenID Connect).
|
||||
|
||||
## Índice
|
||||
|
||||
1. [Configuración General de OIDC](#configuración-general-de-oidc)
|
||||
2. [Authentik](#authentik)
|
||||
3. [Authelia](#authelia)
|
||||
4. [KeyCloak](#keycloak)
|
||||
5. [Resolución de Problemas](#resolución-de-problemas)
|
||||
|
||||
## Configuración General de OIDC
|
||||
|
||||
Para habilitar la integración OIDC en OxiCloud, necesitará establecer las siguientes variables de entorno:
|
||||
|
||||
```bash
|
||||
# Habilitar OIDC
|
||||
OXICLOUD_ENABLE_OIDC=true
|
||||
|
||||
# Configuración para cada proveedor OIDC (puede configurar múltiples proveedores)
|
||||
OXICLOUD_OIDC_PROVIDER_<NOMBRE>_NAME="Nombre Visible"
|
||||
OXICLOUD_OIDC_PROVIDER_<NOMBRE>_CLIENT_ID="su-client-id"
|
||||
OXICLOUD_OIDC_PROVIDER_<NOMBRE>_CLIENT_SECRET="su-client-secret"
|
||||
OXICLOUD_OIDC_PROVIDER_<NOMBRE>_DISCOVERY_URL="https://proveedor.example.com/.well-known/openid-configuration"
|
||||
OXICLOUD_OIDC_PROVIDER_<NOMBRE>_REDIRECT_URI="https://su-oxicloud.example.com/oidc/callback/<nombre>"
|
||||
OXICLOUD_OIDC_PROVIDER_<NOMBRE>_SCOPES="openid profile email"
|
||||
OXICLOUD_OIDC_PROVIDER_<NOMBRE>_USER_ID_ATTRIBUTE="sub"
|
||||
OXICLOUD_OIDC_PROVIDER_<NOMBRE>_DEFAULT_ROLE="user"
|
||||
OXICLOUD_OIDC_PROVIDER_<NOMBRE>_AUTO_CREATE_USERS="true"
|
||||
```
|
||||
|
||||
## Authentik
|
||||
|
||||
[Authentik](https://goauthentik.io/) es una plataforma de identidad de código abierto que proporciona autenticación, autorización y gestión de usuarios.
|
||||
|
||||
### 1. Configurar una aplicación en Authentik
|
||||
|
||||
1. Inicia sesión en tu panel de administración de Authentik
|
||||
2. Ve a "Applications" → "Create"
|
||||
3. Introduce un nombre para tu aplicación (ej. "OxiCloud")
|
||||
4. Selecciona "OAuth2/OpenID Provider" como tipo de proveedor
|
||||
5. En la configuración de OAuth2:
|
||||
- **Redirect URI/Callback URL**: `https://su-oxicloud.example.com/oidc/callback/authentik`
|
||||
- **Client Type**: Confidential
|
||||
- **Client ID**: Se generará automáticamente (anótalo)
|
||||
- **Client Secret**: Se generará automáticamente (anótalo)
|
||||
- **Scopes**: openid, email, profile
|
||||
6. En la configuración de UI:
|
||||
- **Launch URL**: `https://su-oxicloud.example.com/`
|
||||
- **Icon**: Opcional, puedes subir un icono para OxiCloud
|
||||
|
||||
### 2. Configurar OxiCloud para Authentik
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3'
|
||||
services:
|
||||
oxicloud:
|
||||
image: oxicloud:latest
|
||||
environment:
|
||||
# Configuración general
|
||||
OXICLOUD_ENABLE_OIDC: "true"
|
||||
|
||||
# Configuración de Authentik
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHENTIK_NAME: "Authentik"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHENTIK_CLIENT_ID: "tu-client-id-de-authentik"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHENTIK_CLIENT_SECRET: "tu-client-secret-de-authentik"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHENTIK_DISCOVERY_URL: "https://authentik.example.com/application/o/oxicloud/.well-known/openid-configuration"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHENTIK_REDIRECT_URI: "https://oxicloud.example.com/oidc/callback/authentik"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHENTIK_SCOPES: "openid profile email"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHENTIK_USER_ID_ATTRIBUTE: "sub"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHENTIK_DEFAULT_ROLE: "user"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHENTIK_AUTO_CREATE_USERS: "true"
|
||||
ports:
|
||||
- "8085:8085"
|
||||
volumes:
|
||||
- ./storage:/app/storage
|
||||
```
|
||||
|
||||
## Authelia
|
||||
|
||||
[Authelia](https://www.authelia.com/) es una solución de autenticación multi-factor de código abierto.
|
||||
|
||||
### 1. Configurar Authelia para OxiCloud
|
||||
|
||||
Edita tu configuración de Authelia (`configuration.yml`):
|
||||
|
||||
```yaml
|
||||
identity_providers:
|
||||
oidc:
|
||||
hmac_secret: tu-secreto-seguro # Cambia esto por un valor aleatorio seguro
|
||||
issuer_private_key: /config/private.pem # Ruta a tu clave privada
|
||||
cors:
|
||||
endpoints: ['authorization', 'token', 'revocation', 'introspection']
|
||||
allowed_origins:
|
||||
- https://oxicloud.example.com
|
||||
clients:
|
||||
- id: oxicloud
|
||||
description: OxiCloud
|
||||
secret: tu-client-secret-seguro # Cambia esto
|
||||
public: false
|
||||
authorization_policy: two_factor
|
||||
redirect_uris:
|
||||
- https://oxicloud.example.com/oidc/callback/authelia
|
||||
scopes: ['openid', 'profile', 'email', 'groups']
|
||||
userinfo_signing_algorithm: none
|
||||
```
|
||||
|
||||
### 2. Configurar OxiCloud para Authelia
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3'
|
||||
services:
|
||||
oxicloud:
|
||||
image: oxicloud:latest
|
||||
environment:
|
||||
# Configuración general
|
||||
OXICLOUD_ENABLE_OIDC: "true"
|
||||
|
||||
# Configuración de Authelia
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHELIA_NAME: "Authelia"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHELIA_CLIENT_ID: "oxicloud"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHELIA_CLIENT_SECRET: "tu-client-secret-seguro"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHELIA_DISCOVERY_URL: "https://authelia.example.com/.well-known/openid-configuration"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHELIA_REDIRECT_URI: "https://oxicloud.example.com/oidc/callback/authelia"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHELIA_SCOPES: "openid profile email groups"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHELIA_USER_ID_ATTRIBUTE: "sub"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHELIA_DEFAULT_ROLE: "user"
|
||||
OXICLOUD_OIDC_PROVIDER_AUTHELIA_AUTO_CREATE_USERS: "true"
|
||||
ports:
|
||||
- "8085:8085"
|
||||
volumes:
|
||||
- ./storage:/app/storage
|
||||
```
|
||||
|
||||
## KeyCloak
|
||||
|
||||
[KeyCloak](https://www.keycloak.org/) es una solución de gestión de identidad y acceso de código abierto.
|
||||
|
||||
### 1. Configurar un cliente en KeyCloak
|
||||
|
||||
1. Inicia sesión en la consola de administración de KeyCloak
|
||||
2. Selecciona tu Reino (Realm)
|
||||
3. Ve a "Clients" → "Create"
|
||||
4. Completa el formulario:
|
||||
- **Client ID**: `oxicloud`
|
||||
- **Client Protocol**: `openid-connect`
|
||||
- **Root URL**: `https://oxicloud.example.com`
|
||||
5. En la configuración del cliente:
|
||||
- **Access Type**: `confidential`
|
||||
- **Valid Redirect URIs**: `https://oxicloud.example.com/oidc/callback/keycloak`
|
||||
- **Web Origins**: `https://oxicloud.example.com` (o `+` para permitir todos los orígenes)
|
||||
6. Guarda la configuración
|
||||
7. Ve a la pestaña "Credentials" y copia el "Secret" generado
|
||||
|
||||
### 2. Configurar OxiCloud para KeyCloak
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3'
|
||||
services:
|
||||
oxicloud:
|
||||
image: oxicloud:latest
|
||||
environment:
|
||||
# Configuración general
|
||||
OXICLOUD_ENABLE_OIDC: "true"
|
||||
|
||||
# Configuración de KeyCloak
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_NAME: "KeyCloak"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_CLIENT_ID: "oxicloud"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_CLIENT_SECRET: "tu-client-secret-de-keycloak"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_DISCOVERY_URL: "https://keycloak.example.com/realms/tu-realm/.well-known/openid-configuration"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_REDIRECT_URI: "https://oxicloud.example.com/oidc/callback/keycloak"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_SCOPES: "openid profile email"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_USER_ID_ATTRIBUTE: "sub"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_DEFAULT_ROLE: "user"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_AUTO_CREATE_USERS: "true"
|
||||
ports:
|
||||
- "8085:8085"
|
||||
volumes:
|
||||
- ./storage:/app/storage
|
||||
```
|
||||
|
||||
## Resolución de Problemas
|
||||
|
||||
### Error: "Failed to discover OIDC provider"
|
||||
|
||||
Este error ocurre cuando OxiCloud no puede acceder al punto de descubrimiento del proveedor OIDC.
|
||||
|
||||
**Soluciones:**
|
||||
1. Verifica que la URL de descubrimiento sea correcta
|
||||
2. Asegúrate de que OxiCloud pueda acceder a la URL (verifique firewalls, DNS, etc.)
|
||||
3. Si tu proveedor utiliza un certificado autofirmado, asegúrate de configurar la confianza adecuada
|
||||
|
||||
### Error: "Invalid redirect URI"
|
||||
|
||||
Tu proveedor OIDC rechaza la URI de redirección.
|
||||
|
||||
**Soluciones:**
|
||||
1. Asegúrate de que la URI de redirección configurada en OxiCloud coincida exactamente con la registrada en tu proveedor OIDC
|
||||
2. Verifica que no haya diferencias en protocolo (http vs https), puerto o ruta
|
||||
|
||||
### Error: "User does not exist and auto-creation is disabled"
|
||||
|
||||
**Soluciones:**
|
||||
1. Habilita la creación automática de usuarios: `OXICLOUD_OIDC_PROVIDER_<NOMBRE>_AUTO_CREATE_USERS="true"`
|
||||
2. O crea manualmente el usuario en OxiCloud antes de intentar iniciar sesión con OIDC
|
||||
|
||||
### Error: "Could not extract user ID from claim"
|
||||
|
||||
OxiCloud no puede encontrar el atributo de ID de usuario especificado en los claims del token.
|
||||
|
||||
**Soluciones:**
|
||||
1. Verifica que el atributo configurado (`USER_ID_ATTRIBUTE`) exista en los claims del token
|
||||
2. Prueba con un atributo diferente, como "sub", "email" o "preferred_username"
|
||||
3. Configura tu proveedor OIDC para incluir el atributo necesario en los tokens
|
||||
@@ -0,0 +1,702 @@
|
||||
# OIDC Integration for OxiCloud
|
||||
|
||||
This document outlines the implementation plan for adding OpenID Connect (OIDC) support to OxiCloud, enabling Single Sign-On (SSO) with identity providers like Authentik, Authelia, KeyCloak, and others.
|
||||
|
||||
## Overview
|
||||
|
||||
OpenID Connect (OIDC) is an identity layer built on top of the OAuth 2.0 protocol. It allows clients to verify the identity of end-users based on the authentication performed by an authorization server, as well as to obtain basic profile information about the end-user.
|
||||
|
||||
Implementing OIDC in OxiCloud will:
|
||||
1. Allow users to authenticate using their existing identity provider (IdP) credentials
|
||||
2. Reduce the need for separate username/password management in OxiCloud
|
||||
3. Enhance security by leveraging modern authentication best practices
|
||||
4. Provide a seamless experience for users already using SSO in their environment
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Add OIDC Configuration Options
|
||||
|
||||
Extend the `AuthConfig` struct in `src/common/config.rs`:
|
||||
|
||||
```rust
|
||||
pub struct AuthConfig {
|
||||
pub jwt_secret: String,
|
||||
pub access_token_expiry_secs: i64,
|
||||
pub refresh_token_expiry_secs: i64,
|
||||
pub hash_memory_cost: u32,
|
||||
pub hash_time_cost: u32,
|
||||
|
||||
// New OIDC configuration
|
||||
pub enable_oidc: bool,
|
||||
pub oidc_providers: Vec<OidcProviderConfig>,
|
||||
}
|
||||
|
||||
pub struct OidcProviderConfig {
|
||||
pub name: String, // Display name (e.g., "Authentik", "KeyCloak")
|
||||
pub client_id: String, // OIDC client ID
|
||||
pub client_secret: String, // OIDC client secret
|
||||
pub discovery_url: String, // OIDC discovery URL (.well-known/openid-configuration)
|
||||
pub redirect_uri: String, // Redirect URI after authentication
|
||||
pub scopes: Vec<String>, // Scopes to request
|
||||
pub user_id_attribute: String, // Which claim to use as user ID
|
||||
pub default_role: String, // Default role for new users
|
||||
pub auto_create_users: bool, // Create users on first login
|
||||
}
|
||||
|
||||
impl Default for AuthConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// Existing defaults...
|
||||
|
||||
// OIDC defaults
|
||||
enable_oidc: false,
|
||||
oidc_providers: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Update the environment variable handling in `AppConfig::from_env()` to include OIDC configurations.
|
||||
|
||||
### 2. Create OIDC Service Implementation
|
||||
|
||||
Add a new file `src/domain/services/oidc_service.rs`:
|
||||
|
||||
```rust
|
||||
use openid::{Client, Discovered, DiscoveredClient, Options, Token, StandardClaims};
|
||||
use std::sync::Arc;
|
||||
use reqwest::Client as HttpClient;
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::common::config::OidcProviderConfig;
|
||||
use crate::domain::entities::user::{User, UserRole};
|
||||
use crate::domain::repositories::user_repository::UserRepository;
|
||||
use crate::common::errors::{DomainError, ErrorKind};
|
||||
|
||||
pub struct OidcService {
|
||||
providers: Vec<OidcProvider>,
|
||||
user_repository: Arc<dyn UserRepository>,
|
||||
}
|
||||
|
||||
struct OidcProvider {
|
||||
config: OidcProviderConfig,
|
||||
client: DiscoveredClient,
|
||||
}
|
||||
|
||||
impl OidcService {
|
||||
pub async fn new(
|
||||
configs: Vec<OidcProviderConfig>,
|
||||
user_repository: Arc<dyn UserRepository>,
|
||||
) -> Result<Self, DomainError> {
|
||||
let http_client = HttpClient::new();
|
||||
let mut providers = Vec::new();
|
||||
|
||||
for config in configs {
|
||||
let client = openid::Client::discover(
|
||||
http_client.clone(),
|
||||
&config.client_id,
|
||||
&config.client_secret,
|
||||
&config.redirect_uri,
|
||||
&config.discovery_url,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"OIDC",
|
||||
format!("Failed to discover OIDC provider {}: {}", config.name, e)
|
||||
))?;
|
||||
|
||||
providers.push(OidcProvider {
|
||||
config: config.clone(),
|
||||
client,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
providers,
|
||||
user_repository,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_provider(&self, provider_name: &str) -> Option<&OidcProvider> {
|
||||
self.providers.iter().find(|p| p.config.name == provider_name)
|
||||
}
|
||||
|
||||
pub fn get_providers_info(&self) -> Vec<OidcProviderInfo> {
|
||||
self.providers.iter().map(|p| OidcProviderInfo {
|
||||
name: p.config.name.clone(),
|
||||
display_name: p.config.name.clone(),
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn generate_authorization_url(&self, provider_name: &str, state: &str) -> Result<String, DomainError> {
|
||||
let provider = self.get_provider(provider_name).ok_or_else(|| DomainError::new(
|
||||
ErrorKind::NotFound,
|
||||
"OIDC",
|
||||
format!("Provider {} not found", provider_name)
|
||||
))?;
|
||||
|
||||
let mut options = Options::default();
|
||||
options.scope = Some(provider.config.scopes.join(" "));
|
||||
|
||||
let auth_url = provider.client.auth_url(&options, Some(state));
|
||||
Ok(auth_url.to_string())
|
||||
}
|
||||
|
||||
pub async fn process_callback(
|
||||
&self,
|
||||
provider_name: &str,
|
||||
code: &str,
|
||||
state: &str
|
||||
) -> Result<(User, Token<Discovered, StandardClaims>), DomainError> {
|
||||
let provider = self.get_provider(provider_name).ok_or_else(|| DomainError::new(
|
||||
ErrorKind::NotFound,
|
||||
"OIDC",
|
||||
format!("Provider {} not found", provider_name)
|
||||
))?;
|
||||
|
||||
// Exchange code for token
|
||||
let token = provider.client.request_token(code).await.map_err(|e| DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"OIDC",
|
||||
format!("Failed to exchange code for token: {}", e)
|
||||
))?;
|
||||
|
||||
// Extract user information from claims
|
||||
let claims = token.id_token.payload().clone();
|
||||
|
||||
// Get user ID from configured attribute
|
||||
let user_id_attr = &provider.config.user_id_attribute;
|
||||
let external_user_id = match user_id_attr.as_str() {
|
||||
"sub" => claims.sub.clone(),
|
||||
"email" => claims.email.clone().unwrap_or_default(),
|
||||
// Add other standard claims as needed
|
||||
_ => claims.additional_claims.get(user_id_attr)
|
||||
.and_then(|v| v.as_str().map(|s| s.to_string()))
|
||||
.unwrap_or_default(),
|
||||
};
|
||||
|
||||
if external_user_id.is_empty() {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::InvalidInput,
|
||||
"OIDC",
|
||||
format!("Could not extract user ID from claim '{}'", user_id_attr)
|
||||
));
|
||||
}
|
||||
|
||||
// Check if user exists with this external ID
|
||||
let mapped_user_id = format!("{}:{}", provider_name, external_user_id);
|
||||
|
||||
let user = match self.user_repository.get_user_by_external_id(&mapped_user_id).await {
|
||||
Ok(existing_user) => existing_user,
|
||||
Err(_) => {
|
||||
// User doesn't exist, create if allowed
|
||||
if !provider.config.auto_create_users {
|
||||
return Err(DomainError::new(
|
||||
ErrorKind::AccessDenied,
|
||||
"OIDC",
|
||||
"User does not exist and auto-creation is disabled"
|
||||
));
|
||||
}
|
||||
|
||||
// Get user information from claims
|
||||
let email = claims.email.clone().unwrap_or_else(||
|
||||
format!("{}@oidc.oxicloud.local", Uuid::new_v4())
|
||||
);
|
||||
|
||||
let username = claims.preferred_username.clone()
|
||||
.or_else(|| claims.email.clone())
|
||||
.unwrap_or_else(|| format!("user_{}", Uuid::new_v4()));
|
||||
|
||||
// Create the user
|
||||
let role = match provider.config.default_role.as_str() {
|
||||
"admin" => UserRole::Admin,
|
||||
_ => UserRole::User,
|
||||
};
|
||||
|
||||
// Default quota
|
||||
let quota = 1024 * 1024 * 1024; // 1GB
|
||||
|
||||
let mut new_user = User::new(
|
||||
username,
|
||||
email,
|
||||
Uuid::new_v4().to_string(), // Random password, not used for OIDC
|
||||
role,
|
||||
quota,
|
||||
)?;
|
||||
|
||||
// Set external ID
|
||||
new_user.set_external_id(Some(mapped_user_id));
|
||||
|
||||
// Save user
|
||||
self.user_repository.create_user(new_user).await?
|
||||
}
|
||||
};
|
||||
|
||||
Ok((user, token))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize)]
|
||||
pub struct OidcProviderInfo {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Update User Entity
|
||||
|
||||
Modify `src/domain/entities/user.rs` to support external IDs for OIDC users:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct User {
|
||||
// Existing fields...
|
||||
external_id: Option<String>, // For OIDC users: "provider:external_id"
|
||||
}
|
||||
|
||||
impl User {
|
||||
// Existing methods...
|
||||
|
||||
pub fn external_id(&self) -> Option<&str> {
|
||||
self.external_id.as_deref()
|
||||
}
|
||||
|
||||
pub fn set_external_id(&mut self, external_id: Option<String>) {
|
||||
self.external_id = external_id;
|
||||
}
|
||||
|
||||
pub fn is_oidc_user(&self) -> bool {
|
||||
self.external_id.is_some()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Update Database Schema
|
||||
|
||||
Add a new column to the users table in `db/schema.sql`:
|
||||
|
||||
```sql
|
||||
ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS external_id VARCHAR(255) UNIQUE;
|
||||
```
|
||||
|
||||
### 5. Update the Auth Application Service
|
||||
|
||||
Modify `src/application/services/auth_application_service.rs` to add OIDC methods:
|
||||
|
||||
```rust
|
||||
use crate::domain::services::oidc_service::{OidcService, OidcProviderInfo};
|
||||
use crate::application::dtos::user_dto::{OidcAuthUrlDto, OidcCallbackDto, OidcProviderDto};
|
||||
|
||||
impl AuthApplicationService {
|
||||
// Add OIDC service
|
||||
pub fn with_oidc_service(mut self, oidc_service: Arc<OidcService>) -> Self {
|
||||
self.oidc_service = Some(oidc_service);
|
||||
self
|
||||
}
|
||||
|
||||
// Get available OIDC providers
|
||||
pub fn get_oidc_providers(&self) -> Result<Vec<OidcProviderDto>, DomainError> {
|
||||
let oidc_service = self.oidc_service.as_ref()
|
||||
.ok_or_else(|| DomainError::new(
|
||||
ErrorKind::UnsupportedOperation,
|
||||
"Auth",
|
||||
"OIDC is not configured"
|
||||
))?;
|
||||
|
||||
let providers = oidc_service.get_providers_info();
|
||||
Ok(providers.into_iter().map(OidcProviderDto::from).collect())
|
||||
}
|
||||
|
||||
// Generate authorization URL
|
||||
pub fn generate_oidc_auth_url(&self, dto: OidcAuthUrlDto) -> Result<String, DomainError> {
|
||||
let oidc_service = self.oidc_service.as_ref()
|
||||
.ok_or_else(|| DomainError::new(
|
||||
ErrorKind::UnsupportedOperation,
|
||||
"Auth",
|
||||
"OIDC is not configured"
|
||||
))?;
|
||||
|
||||
oidc_service.generate_authorization_url(&dto.provider, &dto.state)
|
||||
}
|
||||
|
||||
// Process OIDC callback
|
||||
pub async fn process_oidc_callback(&self, dto: OidcCallbackDto) -> Result<AuthResponseDto, DomainError> {
|
||||
let oidc_service = self.oidc_service.as_ref()
|
||||
.ok_or_else(|| DomainError::new(
|
||||
ErrorKind::UnsupportedOperation,
|
||||
"Auth",
|
||||
"OIDC is not configured"
|
||||
))?;
|
||||
|
||||
let (user, _token) = oidc_service.process_callback(
|
||||
&dto.provider,
|
||||
&dto.code,
|
||||
&dto.state
|
||||
).await?;
|
||||
|
||||
// Generate access token
|
||||
let access_token = self.auth_service.generate_access_token(&user)
|
||||
.map_err(DomainError::from)?;
|
||||
|
||||
// Generate refresh token
|
||||
let refresh_token = self.auth_service.generate_refresh_token();
|
||||
|
||||
// Create session
|
||||
let session = Session::new(
|
||||
user.id().to_string(),
|
||||
refresh_token.clone(),
|
||||
None,
|
||||
None,
|
||||
self.auth_service.refresh_token_expiry_days(),
|
||||
);
|
||||
|
||||
self.session_storage.create_session(session).await?;
|
||||
|
||||
// Return auth response
|
||||
Ok(AuthResponseDto {
|
||||
user: UserDto::from(user),
|
||||
access_token,
|
||||
refresh_token,
|
||||
token_type: "Bearer".to_string(),
|
||||
expires_in: self.auth_service.refresh_token_expiry_secs(),
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Add Auth Handler Routes for OIDC
|
||||
|
||||
Update `src/interfaces/api/handlers/auth_handler.rs`:
|
||||
|
||||
```rust
|
||||
pub fn auth_routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/register", post(register))
|
||||
.route("/login", post(login))
|
||||
.route("/refresh", post(refresh_token))
|
||||
.route("/me", get(get_current_user))
|
||||
.route("/change-password", put(change_password))
|
||||
.route("/logout", post(logout))
|
||||
// Add OIDC routes
|
||||
.route("/oidc/providers", get(get_oidc_providers))
|
||||
.route("/oidc/auth", post(generate_oidc_auth_url))
|
||||
.route("/oidc/callback", post(process_oidc_callback))
|
||||
}
|
||||
|
||||
// Get available OIDC providers
|
||||
async fn get_oidc_providers(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let auth_service = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
||||
|
||||
match auth_service.auth_application_service.get_oidc_providers() {
|
||||
Ok(providers) => Ok((StatusCode::OK, Json(providers))),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
// Generate OIDC authorization URL
|
||||
async fn generate_oidc_auth_url(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(dto): Json<OidcAuthUrlDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let auth_service = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
||||
|
||||
match auth_service.auth_application_service.generate_oidc_auth_url(dto) {
|
||||
Ok(url) => Ok((StatusCode::OK, Json(json!({ "url": url })))),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
// Process OIDC callback
|
||||
async fn process_oidc_callback(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(dto): Json<OidcCallbackDto>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let auth_service = state.auth_service.as_ref()
|
||||
.ok_or_else(|| AppError::internal_error("Servicio de autenticación no configurado"))?;
|
||||
|
||||
match auth_service.auth_application_service.process_oidc_callback(dto).await {
|
||||
Ok(auth_response) => Ok((StatusCode::OK, Json(auth_response))),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Update DTOs for OIDC
|
||||
|
||||
Create new DTOs in `src/application/dtos/user_dto.rs`:
|
||||
|
||||
```rust
|
||||
use crate::domain::services::oidc_service::OidcProviderInfo;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct OidcProviderDto {
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
}
|
||||
|
||||
impl From<OidcProviderInfo> for OidcProviderDto {
|
||||
fn from(info: OidcProviderInfo) -> Self {
|
||||
Self {
|
||||
name: info.name,
|
||||
display_name: info.display_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct OidcAuthUrlDto {
|
||||
pub provider: String,
|
||||
pub state: String,
|
||||
pub redirect_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct OidcCallbackDto {
|
||||
pub provider: String,
|
||||
pub code: String,
|
||||
pub state: String,
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Add Frontend Integration
|
||||
|
||||
Create a new JavaScript file `static/js/oidcAuth.js`:
|
||||
|
||||
```javascript
|
||||
// OIDC Authentication Module
|
||||
const oidcAuth = {
|
||||
// Get available OIDC providers
|
||||
async getProviders() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/oidc/providers');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get OIDC providers: ${response.statusText}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Error fetching OIDC providers:', error);
|
||||
return [];
|
||||
}
|
||||
},
|
||||
|
||||
// Generate random state for CSRF protection
|
||||
generateState() {
|
||||
const array = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(array);
|
||||
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
||||
},
|
||||
|
||||
// Start OIDC authentication flow
|
||||
async startAuth(providerName) {
|
||||
try {
|
||||
// Generate and store state
|
||||
const state = this.generateState();
|
||||
localStorage.setItem('oidc_state', state);
|
||||
|
||||
// Get authorization URL
|
||||
const response = await fetch('/api/auth/oidc/auth', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider: providerName,
|
||||
state: state,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to get auth URL: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Redirect to authorization URL
|
||||
window.location.href = data.url;
|
||||
} catch (error) {
|
||||
console.error('Error starting OIDC auth:', error);
|
||||
alert('Failed to start authentication. Please try again.');
|
||||
}
|
||||
},
|
||||
|
||||
// Handle OIDC callback
|
||||
async handleCallback() {
|
||||
// Parse URL parameters
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const code = urlParams.get('code');
|
||||
const state = urlParams.get('state');
|
||||
const error = urlParams.get('error');
|
||||
|
||||
// Check for errors
|
||||
if (error) {
|
||||
console.error('OIDC authentication error:', error);
|
||||
alert(`Authentication failed: ${error}`);
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify code and state
|
||||
if (!code || !state) {
|
||||
console.error('Missing code or state in callback');
|
||||
alert('Authentication failed: Invalid response');
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify state matches
|
||||
const savedState = localStorage.getItem('oidc_state');
|
||||
if (state !== savedState) {
|
||||
console.error('State mismatch - potential CSRF attack');
|
||||
alert('Authentication failed: Invalid state');
|
||||
window.location.href = '/login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear stored state
|
||||
localStorage.removeItem('oidc_state');
|
||||
|
||||
try {
|
||||
// Extract provider from URL path or from saved data
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const provider = localStorage.getItem('oidc_provider') ||
|
||||
(pathParts.length > 2 ? pathParts[2] : 'default');
|
||||
|
||||
// Process callback
|
||||
const response = await fetch('/api/auth/oidc/callback', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
provider: provider,
|
||||
code: code,
|
||||
state: state,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to process callback: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const authData = await response.json();
|
||||
|
||||
// Store auth data and redirect to dashboard
|
||||
localStorage.setItem('auth_token', authData.access_token);
|
||||
localStorage.setItem('refresh_token', authData.refresh_token);
|
||||
localStorage.setItem('user', JSON.stringify(authData.user));
|
||||
|
||||
window.location.href = '/index.html';
|
||||
} catch (error) {
|
||||
console.error('Error handling OIDC callback:', error);
|
||||
alert('Failed to complete authentication. Please try again.');
|
||||
window.location.href = '/login.html';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Check if current page is callback page
|
||||
if (window.location.pathname.includes('/oidc/callback')) {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
oidcAuth.handleCallback();
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 9. Update Login Page
|
||||
|
||||
Add OIDC login buttons to `static/login.html`:
|
||||
|
||||
```html
|
||||
<!-- OIDC Login Section -->
|
||||
<div class="oidc-login">
|
||||
<h3>Login with SSO</h3>
|
||||
<div id="oidc-providers">
|
||||
<!-- OIDC provider buttons will be added here dynamically -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load OIDC providers
|
||||
async function loadOidcProviders() {
|
||||
try {
|
||||
const providers = await oidcAuth.getProviders();
|
||||
const providersContainer = document.getElementById('oidc-providers');
|
||||
|
||||
if (providers.length === 0) {
|
||||
providersContainer.innerHTML = '<p>No SSO providers configured.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const buttons = providers.map(provider => {
|
||||
return `<button
|
||||
class="btn btn-oidc"
|
||||
data-provider="${provider.name}"
|
||||
onclick="startOidcAuth('${provider.name}')"
|
||||
>
|
||||
Login with ${provider.display_name}
|
||||
</button>`;
|
||||
}).join('');
|
||||
|
||||
providersContainer.innerHTML = buttons;
|
||||
} catch (error) {
|
||||
console.error('Failed to load OIDC providers:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Start OIDC authentication
|
||||
function startOidcAuth(providerName) {
|
||||
localStorage.setItem('oidc_provider', providerName);
|
||||
oidcAuth.startAuth(providerName);
|
||||
}
|
||||
|
||||
// Load providers when page loads
|
||||
document.addEventListener('DOMContentLoaded', loadOidcProviders);
|
||||
</script>
|
||||
```
|
||||
|
||||
## Configuration Example
|
||||
|
||||
Here's how to configure OxiCloud to use OIDC with KeyCloak:
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
version: '3'
|
||||
services:
|
||||
oxicloud:
|
||||
image: oxicloud:latest
|
||||
environment:
|
||||
OXICLOUD_ENABLE_OIDC: "true"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_NAME: "KeyCloak"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_CLIENT_ID: "oxicloud"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_CLIENT_SECRET: "your-client-secret"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_DISCOVERY_URL: "https://keycloak.example.com/realms/your-realm/.well-known/openid-configuration"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_REDIRECT_URI: "https://oxicloud.example.com/oidc/callback/keycloak"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_SCOPES: "openid profile email"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_USER_ID_ATTRIBUTE: "sub"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_DEFAULT_ROLE: "user"
|
||||
OXICLOUD_OIDC_PROVIDER_KEYCLOAK_AUTO_CREATE_USERS: "true"
|
||||
ports:
|
||||
- "8085:8085"
|
||||
volumes:
|
||||
- ./storage:/app/storage
|
||||
```
|
||||
|
||||
## Additional Considerations
|
||||
|
||||
1. **Security**: OIDC connections should always use HTTPS. Ensure proper TLS configuration.
|
||||
|
||||
2. **User mapping**: Consider how user attributes from OIDC map to your application (roles, groups, etc.).
|
||||
|
||||
3. **Multiple providers**: The design supports multiple OIDC providers simultaneously.
|
||||
|
||||
4. **Session management**: Implement proper session handling for OIDC users.
|
||||
|
||||
5. **Access control**: Review how OIDC integration affects your application's permission model.
|
||||
|
||||
6. **Testing**: Create separate test IdP configurations for development and testing.
|
||||
Reference in New Issue
Block a user