adding features

This commit is contained in:
DioCrafts
2025-04-02 23:14:12 +02:00
parent a79c335b73
commit 01c81bfb1a
10 changed files with 2450 additions and 0 deletions
+54
View File
@@ -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
+56
View File
@@ -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"
+63
View File
@@ -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!"
+60
View File
@@ -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
+246
View File
@@ -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
+286
View File
@@ -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
+603
View File
@@ -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
+162
View File
@@ -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.
+218
View File
@@ -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
+702
View File
@@ -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.