292 lines
17 KiB
Markdown
292 lines
17 KiB
Markdown
# Pertenencias de activos a dominios · Paso 2.2
|
|
|
|
Assets registra el hecho explícito de que un activo pertenece a un dominio Windows.
|
|
El destino sigue siendo el UUID de un Resource con capacidad `directory:domain`,
|
|
validado contra la identidad estructurada del [paso 2.1](dominios-windows.md).
|
|
No se crea una entidad WindowsDomain ni se deducen relaciones de perfiles,
|
|
puestos, cuentas, IP, FQDN o sistemas operativos.
|
|
|
|
## Modelo, roles y compatibilidad
|
|
|
|
`AssetDirectoryMembership` es un AggregateRoot con identidad UniqueID UUID v7.
|
|
Mantiene compañía, activo, Resource de dominio y revisiones identificadas mediante
|
|
UUID v7. `DirectoryMembershipValues` contiene un solo rol y la fecha de finalización.
|
|
`DirectoryMembershipRevision` conserva valores, actor y momento del cambio.
|
|
|
|
Los roles son `member` y `domain-controller`. Un controlador ya es miembro: no se
|
|
crea otra fila para representar su pertenencia. Un dominio admite tantos miembros
|
|
y controladores como sea necesario, pero cada activo tiene como máximo una
|
|
pertenencia vigente, cualquiera que sea su rol o el dominio.
|
|
|
|
Se reutilizan clasificaciones estables existentes:
|
|
|
|
- Miembro: plantilla de serie `template:laptop`, `template:desktop`,
|
|
`template:server` o rol técnico explícito `server` de AssetInfrastructure.
|
|
- Controlador: plantilla `template:server` o rol técnico explícito `server` de la
|
|
última revisión de infraestructura.
|
|
- Impresoras, periféricos, almacenamiento o equipos de red sin clasificación de
|
|
servidor no se admiten. Una plantilla personalizada necesita una clasificación
|
|
técnica explícita de servidor para entrar en esta versión; no se interpreta su
|
|
nombre o icono. No se amplía el editor de plantillas en este paso.
|
|
|
|
Esto verifica compatibilidad documental con una función de servidor, no que el
|
|
sistema operativo real sea capaz de ejecutar AD. Un servidor Samba compatible
|
|
podría documentarse como controlador sin exigir una comparación de nombres de SO.
|
|
El operador sigue siendo responsable del hecho registrado; no hay sincronización.
|
|
|
|
Las altas, promociones y traslados necesitan un activo operativo y un dominio
|
|
activo. Se consideran operativos los estados técnicos actuales y los valores
|
|
heredados `available`/`in_use`, siguiendo su equivalencia existente. Los estados
|
|
`broken`, `maintenance` y `retired` impiden esas operaciones.
|
|
|
|
## Persistencia e integridad
|
|
|
|
Se añaden únicamente estas tablas de Assets:
|
|
|
|
| Tabla | Contenido y restricciones |
|
|
|---|---|
|
|
| `asset_directory_memberships` | UUID, compañía, activo, dominio, predecesora opcional, rol, creación, finalización y clave vigente. FK compuesta a `(companyId,id)` de assets y a `(resourceId,companyId)` de resource_directory_domains. |
|
|
| `asset_directory_membership_revisions` | UUID, pertenencia, posición, rol y finalización de esa revisión, actor y fecha. FK RESTRICT al registro y al usuario; UNIQUE `(membershipId,position)`. |
|
|
|
|
No se copia el DNS, nombre del dominio ni características del recurso. La FK al
|
|
registro estructurado de Resources exige que el destino sea realmente un dominio
|
|
y, transitivamente, que exista su Resource, plantilla semántica y contexto AD.
|
|
Se añade en la migración de Resources un índice único `(resourceId,companyId)`
|
|
para permitir esa FK compuesta. Es la única ampliación de su esquema en este paso.
|
|
|
|
`activeAssetId` es nullable y lleva el UUID del activo mientras `endedAt IS NULL`.
|
|
Un CHECK obliga a esa correspondencia y exige NULL cuando la pertenencia termina.
|
|
UNIQUE `activeAssetId` impide dos vínculos vigentes del mismo activo. No se puede
|
|
eludir la unicidad mandando NULL: el CHECK también se aplica a escrituras SQL.
|
|
Los registros históricos pueden compartir activo porque sus claves son NULL.
|
|
|
|
Se usa este patrón nullable ya conocido por el repositorio en lugar de una columna
|
|
generada: la expresión propuesta fue rechazada por la MariaDB local al combinarse
|
|
con el esquema de FK. No se desactivan restricciones ni modos estrictos para admitirla.
|
|
La documentación de [columnas generadas de MariaDB](https://mariadb.com/docs/server/reference/sql-statements/data-definition/create/generated-columns)
|
|
recoge sus restricciones; el patrón elegido evita depender de esa expresión.
|
|
|
|
Otros controles:
|
|
|
|
- CHECK de rol y periodo de vigencia válido.
|
|
- FK de predecesora a `(id,companyId,assetId)` de la misma tabla, con unicidad de
|
|
`predecessorId`: cada traslado tiene como máximo una sucesora de ese activo.
|
|
- Índices por compañía/activo y compañía/dominio/rol/vigencia para las consultas.
|
|
- FK e índices no utilizan nombres visibles ni listas de UUID/JSON.
|
|
- El mapper valida UUID, orden y continuidad de revisiones, fechas, cierre final y
|
|
correspondencia entre la última revisión, el rol y el estado de la raíz. Una
|
|
inconsistencia se trata como error de persistencia, nunca como historial vacío.
|
|
|
|
Las migraciones son explícitas y repetibles siguiendo el arranque actual. No se
|
|
usa `sync({ alter: true })`. Las revisiones se insertan y los cambios de raíz se
|
|
actualizan por UUID; no se usa upsert para resolver conflictos de unicidad.
|
|
|
|
## Comandos, idempotencia y concurrencia
|
|
|
|
El repositorio abre una transacción, bloquea compañía y activo y obtiene dentro de
|
|
ella la clasificación y las pertenencias. El servicio público de Resources lee y
|
|
bloquea el dominio en esa misma conexión. La política no hace comprobaciones de
|
|
disponibilidad fuera de la transacción.
|
|
|
|
| Comando | Semántica |
|
|
|---|---|
|
|
| `join` | Crea una pertenencia si no hay ninguna vigente. Si la vigente tiene exactamente dominio y rol solicitados, devuelve esa misma identidad sin revisión. Otro dominio o rol vigente produce `MEMBERSHIP_CONFLICT`. |
|
|
| `role` | Cambia el rol de la pertenencia indicada. Repetir el rol vigente no añade revisiones. Una pertenencia finalizada devuelve `MEMBERSHIP_ENDED`. Promover exige servidor operativo y dominio activo; retirar el rol permite corregir un vínculo aunque el activo o dominio estén inactivos. |
|
|
| `end` | Cierra la pertenencia indicada sin borrar extremos. Repetir devuelve el mismo registro cerrado sin revisión adicional. Una petición antigua no cierra otra pertenencia más reciente. |
|
|
| `transfer` | Exige el UUID de la pertenencia origen. Valida destino, finaliza origen y crea sucesora dentro del mismo commit. Pedir el mismo dominio exige utilizar el comando de rol, no un traslado. |
|
|
|
|
La predecesora conserva la idempotencia de `transfer`: un reintento con igual destino
|
|
y rol inicial devuelve la sucesora existente, incluso si después cambió o finalizó.
|
|
Otro destino o rol para esa misma transición produce conflicto. Dos solicitudes
|
|
concurrentes idénticas devuelven la misma sucesora; dos altas incompatibles para
|
|
el mismo activo se serializan y solo una obtiene una pertenencia vigente.
|
|
|
|
`join` expresa el estado deseado actual, no un identificador de operación indefinido:
|
|
tras finalizar una pertenencia, una nueva llamada a join crea un nuevo periodo.
|
|
Los cambios de rol son asignaciones del rol solicitado sobre la misma pertenencia;
|
|
los cambios opuestos concurrentes se aplican en el orden de serialización, sin
|
|
inventar una versión o ACL no presentes en el contrato.
|
|
|
|
En un traslado se persiste primero el cierre para liberar la clave vigente. Si la
|
|
sucesora o cualquier revisión falla, la transacción revierte también el cierre.
|
|
No hay dos peticiones HTTP ni un estado intermedio confirmado.
|
|
|
|
## Resources, composición y seguridad
|
|
|
|
El contrato público nuevo de Resources es `IDirectoryDomainReader.find`, con
|
|
`UniqueID` de compañía y Resource. Devuelve Maybe de `DirectoryDomainReference`:
|
|
identidad, compañía, capacidad `directory:domain` y estado. Un recurso ordinario,
|
|
inexistente o de otra compañía no se devuelve. No expone nombres ni resúmenes.
|
|
|
|
El composition root de Resources ofrece `services.directoryDomains(transaction)`.
|
|
El servidor lo inyecta en el módulo Assets. El puerto de aplicación no importa
|
|
Sequelize; solo Infrastructure y composición vinculan la transacción. Assets no
|
|
importa repositorios privados de Resources y Resources no depende de Assets.
|
|
Se conserva el grafo modular acíclico.
|
|
|
|
Los consumidores antiguos pueden construir Assets sin el participante: conservan
|
|
sus funciones existentes y no registran rutas de pertenencia. El runtime principal
|
|
lo inyecta expresamente y prepara su migración. No hay descubrimiento dinámico ni
|
|
proveedores ficticios.
|
|
|
|
Las rutas utilizan el middleware de identidad existente. En el runtime real se
|
|
conservan sesión activa, actor de sesión, origen y CSRF de los comandos. No se
|
|
aceptan actor, compañía adicional, privilegios o permisos en el body. Cada consulta
|
|
y comando queda limitado a la compañía de su ruta y a extremos realmente
|
|
pertenecientes a ella. No se presupone una ACL granular: las facultades son las que
|
|
ya proporciona el modelo actual de usuarios autenticados de Metis.
|
|
|
|
## API disponible
|
|
|
|
Base: `/api/asset-directory-memberships`.
|
|
|
|
| Método y ruta | Uso |
|
|
|---|---|
|
|
| `GET /:companyId/:assetId/current` | Pertenencia vigente o `membership: null`. Activo inexistente o de otra compañía devuelve 404. |
|
|
| `GET /:companyId` | Página de pertenencias; filtros opcionales `assetId`, `domainResourceId`, `role`, `includeEnded`, `pageSize`, `cursor`. |
|
|
| `POST /:companyId/:assetId` | Ejecuta uno de los cuatro comandos siguientes. |
|
|
|
|
```json
|
|
{ "action": "join", "domainResourceId": "<uuid>", "role": "member" }
|
|
```
|
|
|
|
```json
|
|
{ "action": "role", "membershipId": "<uuid>", "role": "domain-controller" }
|
|
```
|
|
|
|
```json
|
|
{ "action": "end", "membershipId": "<uuid>" }
|
|
```
|
|
|
|
```json
|
|
{ "action": "transfer", "membershipId": "<uuid-origen>", "domainResourceId": "<uuid-destino>", "role": "member" }
|
|
```
|
|
|
|
Los comandos devuelven 200 con `membership`, tanto al aplicar como al reconocer
|
|
un reintento. La consulta devuelve `memberships` y `nextCursor`; sin filtros usa
|
|
solo vigentes, hasta 50 registros por defecto y entre 1 y 100 por petición. El
|
|
cursor es el último UUID devuelto, con orden estable ascendente y filtros por
|
|
compañía aplicados en cada consulta. No concede acceso ni sustituye autenticación.
|
|
`includeEnded=true` incluye también las pertenencias cerradas; las revisiones de
|
|
rol y finalización están dentro del historial de cada pertenencia.
|
|
|
|
Ejemplos de consulta:
|
|
|
|
```text
|
|
?assetId=<uuid>&includeEnded=true
|
|
?domainResourceId=<uuid>&role=domain-controller
|
|
?domainResourceId=<uuid>&role=member&pageSize=25
|
|
```
|
|
|
|
Roles diferenciados: `role=member` devuelve miembros ordinarios, mientras que sin
|
|
filtro incluye también controladores. Los schemas Zod son estrictos. Los errores
|
|
funcionales usan los códigos `MEMBERSHIP_CONFLICT`, `MEMBERSHIP_ENDED`,
|
|
`MEMBERSHIP_NOT_FOUND`, `ASSET_INCOMPATIBLE`, `ASSET_INACTIVE`, `DOMAIN_INACTIVE`,
|
|
`DOMAIN_NOT_FOUND` y los errores de compañía/activo/entrada ya existentes. Entrada
|
|
inválida devuelve 400, ausencia 404 y conflicto funcional 409; sesión/origen/CSRF
|
|
mantienen las respuestas 401/403 del middleware existente. Los errores inesperados
|
|
siguen el manejador global, sin devolver SQL al cliente.
|
|
|
|
## Ciclo de vida y límites
|
|
|
|
Desactivar un dominio o retirar un activo no finaliza automáticamente los vínculos.
|
|
La pertenencia vigente es un hecho documental conservado que puede requerir revisión,
|
|
no una afirmación de disponibilidad. Se bloquean altas, promociones y traslados
|
|
incompatibles; se permite finalizar o retirar el rol. Cambiar la clasificación
|
|
posteriormente tampoco borra historial ni altera relaciones silenciosamente.
|
|
Un traslado puede abandonar un dominio inactivo si el nuevo destino está activo
|
|
y el activo vuelve a estar operativo.
|
|
|
|
El borrado físico de activos con pertenencias, incluso solo históricas, devuelve
|
|
`ASSET_IN_USE` mediante el guard de referencias existente, sin modificarlo. Las FK
|
|
protegen los dominios estructurados y sus recursos. No hay cascadas nuevas ni
|
|
comandos de borrado de pertenencias o revisiones. Una pertenencia finalizada deja
|
|
libre la clave vigente y permite registrar un nuevo periodo.
|
|
|
|
No se implementan UI, LDAP, descubrimiento, sincronización, cambios de Workstation,
|
|
WorkProfile, FolderMapping o relaciones entre activos.
|
|
|
|
## Preparación del paso 2.3
|
|
|
|
La definición estática `assets:directory-membership` usa los contratos de
|
|
`@metis/relationship-contracts`: origen `assets:asset`, destino `resources:resource`,
|
|
capacidad destino `directory:domain`, dirección dirigida, cardinalidad activa 1:N,
|
|
representación record, historial revisions y escritura owner-command. La definición
|
|
validada se consume en el schema y builder HTTP para el identificador de tipo.
|
|
Las etiquetas tienen claves es/en, sin añadir pantallas.
|
|
|
|
No se crea un registry global ni se implementan RelationshipQueryService o
|
|
EntitySummaryResolver. El paquete común se consume como dependencia workspace, sin
|
|
modificarlo. En el paso 2.3 se necesitarán adaptadores de lectura y resúmenes reales,
|
|
autorización de ambos extremos y composición explícita. Habrá que resolver también
|
|
la validación del namespace de capacidad `directory`: el registry v1 valida los
|
|
namespaces de capacidades como proveedores conocidos; aquí no se registra un
|
|
proveedor ficticio para eludir esa comprobación.
|
|
|
|
## Archivos principales
|
|
|
|
- Assets/common: `directory-memberships.ts`, `directory-membership-definition.ts`,
|
|
índice público y etiquetas en locales es/en.
|
|
- Assets/domain: `directory-membership.aggregate.ts`,
|
|
`directory-membership-revision.entity.ts`, `directory-membership-values.ts`,
|
|
`directory-membership-eligibility.ts`,
|
|
`value-objects/directory-membership-values.vo.ts` y pruebas de contrato/modelo.
|
|
- Assets/application: puerto de repositorio, plan de transiciones, casos de uso y
|
|
builder `directory-membership-*`.
|
|
- Assets/infrastructure: migración, mapper y repositorio `*directory-membership*`,
|
|
router HTTP y composition root del módulo.
|
|
- Resources: puerto y lector `directory-domain-reader.ts`, exportación services,
|
|
composición y un índice compuesto en la migración de identidad del dominio.
|
|
- Servidor: inyección en `identity-runtime.ts`, fixture e integración
|
|
`asset-directory-memberships.*` y script `test:directory-memberships`.
|
|
- Configuración: dependencia workspace de Assets, scripts de pruebas y lockfile.
|
|
|
|
## Verificación de la entrega
|
|
|
|
- Typecheck de Assets, Resources y servidor: correcto.
|
|
- Biome check y ESLint sobre los 30 ficheros de código/configuración de este paso:
|
|
correctos. `git diff --check`: correcto.
|
|
- Suite completa de Assets: **116/116** aprobadas, incluidas las siete nuevas
|
|
pruebas de pertenencias. Se ejecutó el listado configurado por el paquete con
|
|
`--test-isolation=none` para enumerar los casos reales en este entorno.
|
|
- Suite completa de Resources: **53/53** aprobadas con el mismo modo del runner.
|
|
- Integración MariaDB/HTTP: aprobada mediante
|
|
`pnpm --filter @metis/server test:directory-memberships`. Se añade también al
|
|
listado de integración del script test del servidor. Exige explícitamente
|
|
`NODE_ENV=test` y `DB_NAME=metis_desk_test` antes de preparar el esquema o fixtures.
|
|
- La integración prueba sesiones reales de la aplicación de prueba, usuario
|
|
habilitado, origen y CSRF con el middleware existente. Solo el endpoint efímero
|
|
de fixture emite una sesión preparada, sin crear una puerta de autenticación en
|
|
producción ni cambiar el administrador inicial de la base de pruebas.
|
|
- Comprueba varios controladores, rol incompatible, activo/dominio inexistentes,
|
|
recurso ordinario, compañía distinta, consultas e historial, paginación,
|
|
reintentos concurrentes de promoción/traslado y conflicto de altas concurrentes.
|
|
- Verifica SQL real: unicidad vigente, CHECK que rechaza NULL fraudulento, rol,
|
|
compañía y FK al dominio estructurado. Un trigger exclusivamente de fixture,
|
|
limitado a sus UUID y eliminado en finally, fuerza un fallo al insertar la
|
|
sucesora después de cerrar el origen; se comprueba rollback del traslado.
|
|
- Verifica conservación al retirar/desactivar, finalización repetida, borrado
|
|
bloqueado incluso cuando solo queda historial y nueva alta tras finalizar.
|
|
- Build del servidor: correcto. Grafo de 13 paquetes sin ciclos.
|
|
|
|
El primer arranque de integración reprodujo un bloqueo anterior en
|
|
`asset_definitions`: error 1118 al añadir icon pese a tener un esquema lógico
|
|
pequeño. Se resolvió reconstruyendo físicamente **solo esa tabla de
|
|
metis_desk_test** con `ALTER TABLE asset_definitions FORCE`, sin cambiar columnas
|
|
ni borrar registros. No se modificó la migración anterior ni una base de desarrollo
|
|
o producción. La integración completa pasó después de esa reparación del entorno.
|
|
|
|
La instalación offline del lockfile encontró metadatos npm ausentes de una
|
|
dependencia anterior (`@dotenvx/primitives`) durante la política de suministro.
|
|
El único cambio nuevo de dependencias es el enlace workspace a
|
|
`@metis/relationship-contracts`; su entrada del lockfile se ajustó explícitamente.
|
|
Las dependencias ya instaladas permitieron typecheck, importaciones, pruebas y
|
|
build. No se añade ninguna biblioteca externa ni se desactiva la política en la
|
|
configuración del repositorio.
|
|
|
|
No se ejecuta la suite global que limpia otros fixtures de la base de pruebas ni
|
|
la suite de Companies, cuyo código no se modifica en este paso. Los dos fallos de
|
|
textos de Companies documentados en el paso 2.1 no se corrigen ni se presentan
|
|
como comprobados de nuevo aquí. Los fallos transitorios de este paso se han
|
|
corregido y las verificaciones anteriores son sus resultados finales.
|