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