17 KiB
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.
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:servero rol técnico explícitoserverde AssetInfrastructure. - Controlador: plantilla
template:servero rol técnico explícitoserverde 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 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 depredecessorId: 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. |
{ "action": "join", "domainResourceId": "<uuid>", "role": "member" }
{ "action": "role", "membershipId": "<uuid>", "role": "domain-controller" }
{ "action": "end", "membershipId": "<uuid>" }
{ "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:
?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.tsy 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ónasset-directory-memberships.*y scripttest: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=nonepara 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ícitamenteNODE_ENV=testyDB_NAME=metis_desk_testantes 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.