metis-desk/docs/publicacion-de-recursos-compartidos-smb.md
David fe6ec361b7 feat(companies): vincular identidades locales a activos y añadir cuenta de acceso a FolderMapping
- Asociar entornos locales con activos y conservar su trazabilidad.
- Registrar la cuenta de acceso del mapeo con historial de cambios.
- Calcular cobertura documental específica sin alterar la cobertura general.
- Añadir migraciones, validaciones, endpoints protegidos, pruebas y documentación.
2026-10-21 11:25:23 +02:00

102 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Publicación de recursos compartidos SMB v1
## Alcance y propiedad
Assets es propietario de `assets:network-share-publication`: **Asset → publica → Resource con capacidad `network:smb-share`**. Resources conserva el UUID, la UNC, la proyección SMB y su integración con AccessWorkspace. La publicación es un hecho documental registrado por un operador; no verifica disponibilidad, conectividad, permisos ni almacenamiento físico. Un alias UNC no identifica necesariamente al equipo publicador.
Un activo puede publicar múltiples carpetas. Cada recurso tiene cero o un publicador vigente y varios históricos. Un publicador desconocido se representa por ausencia de publicación vigente, sin activos ficticios. Nunca se infiere el publicador del hostname de la UNC.
No existe una clasificación de Assets que certifique capacidad real de servir SMB. Por ello se permite seleccionar explícitamente cualquier activo existente de la compañía con estado `operational`, `available` o `in_use`, siguiendo la política operativa existente. Esto admite Windows Server, NAS, equipos de sobremesa, portátiles y Linux con Samba, sin exigir sistema operativo, dominio, rol `server`/`storage` ni coincidencia entre nombre y UNC. La selección declara la relación; no descubre servicios.
## Modelo e integridad
La migración explícita `migrateNetworkSharePublications` crea `asset_network_share_publications`; no convierte recursos históricos ni crea publicaciones automáticamente. Se ejecuta en la composición de Assets cuando se inyecta el lector público SMB de Resources.
Cada periodo es un `NetworkSharePublication`, con UUID v7 propio, compañía, activo, recurso, actor y fecha de inicio; al finalizar conserva su identidad y añade actor y fecha de cierre. El traslado crea otro periodo con `predecessorId`, sin cambiar el UUID del Resource. El contrato de historial es `lifecycle`: los periodos y su enlace de sucesión conservan el alta, cierre y traslado; no se introduce un historial universal ni una tabla paralela de revisiones.
MariaDB refuerza:
- FK `(companyId,assetId)` hacia Assets y `(resourceId,companyId)` hacia `resource_network_shares`: exige destino SMB estructurado y evita referencias entre compañías.
- `UNIQUE(activeResourceId)`: solo el periodo vigente guarda ahí su `resourceId`; los finalizados tienen NULL.
- CHECK de coherencia entre `endedAt`, `endedBy` y `activeResourceId`, y cierre posterior o igual al inicio.
- `UNIQUE(predecessorId)` y FK compuesta al periodo anterior de la misma compañía y recurso: un periodo tiene como máximo un sucesor de traslado.
- FK de actores a Identity, índices `(companyId,assetId,id)` y `(companyId,resourceId,id)`, sin cascadas destructivas.
No hay comando público para borrar ni reescribir periodos históricos. Los mecanismos existentes de protección de borrado inspeccionan las FK externas de Assets y de la proyección SMB de Resources. Bloquean el borrado físico incluso cuando solo quedan periodos finalizados (`ASSET_IN_USE` / `RESOURCE_IN_USE`). No es necesaria una dependencia inversa Resources → Assets.
## Comandos, concurrencia e idempotencia
`NetworkSharePublicationUseCases` valida entradas estrictas y delega la transacción al repositorio especializado de Assets. Consume únicamente `INetworkShareReader`, mediante la factoría pública de Resources ligada a la transacción.
| Acción | Entrada adicional | Resultado |
| --- | --- | --- |
| `publish` | `assetId`, `resourceId` | Crea un periodo si no hay publicador; devuelve el mismo periodo si ya publica ese activo; otro publicador produce `PUBLICATION_CONFLICT`. |
| `end` | `publicationId` | Cierra ese periodo. Si ya estaba cerrado devuelve su cierre original sin afectar a un sucesor vigente. |
| `transfer` | `publicationId`, `assetId` nuevo | Cierra el periodo indicado y crea su sucesor en una transacción. Exige que esté vigente y que cambie el activo. |
El reintento de un traslado devuelve el sucesor persistido si coinciden predecesor y nuevo activo, incluso si ese sucesor ya terminó o cambió de estado su activo. Un destino diferente para el mismo predecesor produce conflicto. Tras finalizar sin sucesor se utiliza `publish` para comenzar un nuevo periodo. El alta repetida es idempotente respecto de la publicación vigente, no una clave global de idempotencia para todas las altas históricas.
Las mutaciones bloquean la compañía antes de leer periodos y comprobar cardinalidad, compartiendo la serialización por compañía de las escrituras de inventario. Después bloquean el activo destinatario y leen/bloquean el recurso SMB a través de su lector público. El cierre se persiste antes que el sucesor, dentro de la misma transacción. Una excepción revierte ambas escrituras; la unicidad SQL es la garantía final y sus conflictos se traducen a `PUBLICATION_CONFLICT`.
No se permiten nuevas publicaciones ni traslados hacia activos no operativos o recursos inactivos (`ASSET_INACTIVE`, `SMB_RESOURCE_INACTIVE`). La desactivación concurrente queda serializada: o se registra antes de la publicación y esta se rechaza, o se registra después y conserva la publicación. Nunca se cierran periodos automáticamente por desactivación. Finalizar y recuperar un resultado idempotente siguen permitidos.
## Consultas y Typed Relationships
`NetworkSharePublicationQuery` es el segundo `RelationshipQueryService` real. La composición estática mantiene `assets:directory-membership`, añade la definición de publicación y registra `network:smb-share`; los proveedores siguen siendo `assets` y `resources`.
La definición es dirigida, `record`, `lifecycle`, `owner-command`, con múltiples destinos por activo y máximo un origen vigente por recurso. Las vistas incluyen `typeId`, referencias de origen y destino, `origin: { kind: "record", id: publicationId }` y estado `active` / `ended`. No tienen `roleCode` ni atributos arbitrarios.
- Consulta directa: ancla `assets:asset`, dirección `outgoing`.
- Consulta inversa: ancla `resources:resource`, dirección `incoming`.
- `includeEnded: false` selecciona solo publicaciones vigentes; `true` añade las finalizadas. La orientación de las referencias permanece activo → recurso.
- Anclas inexistentes, ajenas o no SMB se consideran no disponibles. El contexto procede de la autorización existente, sin aceptar contextos fabricados por consumidores.
La proyección compartida `readPublicationPage` filtra compañía, ancla y estado en SQL, ordena por UUID y consulta `LIMIT pageSize+1`, con `id > afterId`. Solo retiene el centinela adicional para detectar siguiente página. No hidrata agregados completos ni sus revisiones y no pagina un inventario en memoria. `pageSize` admite 1–100.
`SignedQueryCursor` reutiliza el codec HMAC del proveedor Directory mediante su adaptador existente. Cada instancia tiene clave efímera propia; el ámbito incluye compañía, operador y consulta normalizada (ancla, dirección, estado y tamaño). Publicaciones especializadas, publicaciones tipadas y Directory usan ámbitos/instancias separados: sus cursores son incompatibles. Cambiar filtros, compañía u operador invalida el cursor. El identificador de petición no forma parte del ámbito. Reiniciar o cambiar de instancia de servidor requiere comenzar la paginación de nuevo, igual que en el patrón previo; no se persisten claves nuevas ni datos de negocio en JSON.
Se reutiliza `AssetSummaryResolver`. Resources aporta `NetworkShareSummaryResolver`, exclusivo de proyecciones SMB: una consulta por lote de hasta 100 referencias, deduplicada y acotada por compañía. Conserva orden y repetidos y devuelve `unavailable` sin filtrar nombres para recursos inexistentes, ajenos, ordinarios o dominios Windows. Los resúmenes de recursos inactivos mantienen su estado real.
## HTTP y autorización
Rutas especializadas autenticadas:
- `POST /api/asset-network-share-publications/:companyId`: comando estricto con `action` y los campos de la tabla anterior. Respuesta `{ publication }`.
- `GET /api/asset-network-share-publications/:companyId`: colección con `assetId?`, `resourceId?`, `includeEnded=true|false` (por defecto false), `pageSize` (por defecto 50) y `cursor?`. Respuesta `{ publications, nextCursor }`.
- `GET /api/asset-network-share-publications/:companyId/relationships`: adaptador tipado acotado compartido con el paso 2.4. Parámetros `typeId`, `anchorType`, `anchorId`, `direction`, `includeEnded`, `pageSize` y `cursor?`; respuesta `{ page, summaries }`. Admite únicamente las dos definiciones registradas en este adaptador, sin explorador universal.
La ruta anterior `/api/asset-directory-memberships/:companyId/relationships` conserva el mismo adaptador y sus consultas Directory existentes. Las opciones de dominios y elegibilidad no cambian.
Para obtener el publicador vigente se filtra por `resourceId` con `includeEnded=false`; habrá cero o un elemento. Para el historial se usa el mismo filtro con `includeEnded=true`, siguiendo `nextCursor`. Para las carpetas de un equipo se filtra por `assetId`.
El actor se obtiene de la sesión autenticada, nunca de `operatorId` enviado por el navegador. Las escrituras usan el control global de origen y CSRF de Identity; compañía e identidades se validan en la frontera y en las consultas. Se mantiene la política actual de operadores autenticados de Metis Desk y el contexto de lectura autorizado ya existente, sin añadir un segundo sistema de permisos. Entrada inválida devuelve 400, ausencia funcional 404 y conflicto 409; la lectura tipada mantiene sus errores 400/403/404. Los errores inesperados siguen el mecanismo HTTP centralizado.
## AccessWorkspace y límites
Publicar, finalizar o trasladar no modifica Resource, AccessItem(resource), concesiones, necesidades, cobertura, WorkProfile ni FolderMapping. Tampoco asigna dominios, cuentas locales ni grupos. La integración automática del paso 3.2 permanece en Resources/Companies y no interviene en estos comandos.
No se añade UI. Los contratos, identificadores de periodo, lectura paginada, resúmenes y comandos dejan preparada la presentación del paso 3.5. Local Identities y ampliaciones de FolderMapping pertenecen al paso 3.4. No se documentan puertos de switch, disponibilidad técnica ni topología de almacenamiento.
## Validación
La suite `apps/server/src/network-share-publications.integration.test.ts` exige `NODE_ENV=test` y `DB_NAME=metis_desk_test`; genera y elimina exclusivamente sus compañías y actores de fixture. Cubre periodos, variantes de activo, cardinalidad SQL, compañía, borrado histórico, rollback por fallo de inserción, altas/traslados/cierres concurrentes, desactivación concurrente, cursores, SQL keyset, resolución por lote, accesos sin modificaciones y HTTP con sesión/origen/CSRF. Las pruebas de dominio viven en `modules/assets/src/api/domain/network-share-publication.test.ts`.
Comando específico: `pnpm --filter @metis/server test:network-share-publications`. Deben ejecutarse también las regresiones de SMB, accesos y dominios, las suites de Assets/Resources/relationship-contracts, los typechecks afectados, Biome, ESLint, build del servidor y `git diff --check`.
Fallos previos conocidos ajenos a esta implementación: dos expectativas de texto en Companies y TS2375 en `packages/shadcn-ui/src/components/calendar.tsx`. No se cambia vendor ni se ajustan esas pruebas.
### Resultado de la verificación del paso 3.3
- Typechecks de `@metis/assets`, `@metis/resources`, `@metis/relationship-contracts`, `@metis/server` y `@metis/web`: correctos (`pnpm --filter … typecheck`).
- Suites de Assets: 131/131; Resources: 80/80; contratos tipados: 13/13. Se ejecutaron los scripts con `--test-isolation=none` para comprobar todos los casos.
- Integración MariaDB de publicaciones, SMB, accesos y Directory: 45/45. Regresiones adicionales de inventario, infraestructura, sedes y propagación de plantillas: 4/4. La suite nueva de publicaciones (10 casos incluidos en los 45) también se repitió con esas regresiones.
- `biome check` sobre los 65 ficheros TypeScript/TSX/JSON modificados o nuevos del trabajo acumulado: correcto, sin avisos; `eslint` sobre los 58 ficheros TS/TSX: correcto.
- `pnpm --filter @metis/server build`, revisión del grafo de dependencias de producción de 13 paquetes y `git diff --check`: correctos, sin ciclos.
- Companies: 171/173, con las dos expectativas previas en `companies-page.test.tsx:180` y `:210`. Typecheck directo del paquete vendor: reproduce TS2375 en `calendar.tsx:165`; no se modifica.
Todas las integraciones se ejecutaron con `DB_NAME=metis_desk_test NODE_ENV=test node --env-file=../../.env --import tsx --test --test-isolation=none --test-concurrency=1 …` desde `apps/server`, sin intervenir en datos de compañías reales.
## Identidades locales y cuenta del mapeo (paso 3.4)
La clasificación local de entornos y la cuenta documental de cada FolderMapping se describen en [identidades locales y cuentas de carpetas](identidades-locales-y-cuentas-de-carpetas.md). La publicación sigue sin determinar quién accede a la carpeta ni qué cuenta utiliza un perfil.