Idioma: English Español
Bounded Context: Identity
Aggregate Root: Tenant
Modulo: Ums.Domain.Identity.Tenant
Estado: Produccion
Tenant es la unidad organizativa raiz del sistema. Representa a una empresa o division que usa UMS como plataforma de gestion de identidades. Agrupa a todos los usuarios, ramas (Branch), configuraciones de branding (Branding) y proveedores de identidad (IdentityProvider) bajo un espacio de nombres unico y aislado, y tambien indica si el tenant es el responsable de gestion de su propio portal interno de UMS.
Branch, Branding e IdentityProvider como entidades propias.IdpStrategy) a nivel de dominio.El portal interno de UMS utiliza su propia ruta de autorizacion para los administradores del tenant. Esta ruta es distinta del flujo externo de autenticacion por API y se gobierna por scope de tenant, roles, permisos e IsManagementOwner.
Esta frontera se formaliza en ADR-0077.
| Escenario | Regla |
|---|---|
| Un administrador del tenant entra al portal de UMS | Permitido solo si pertenece al tenant y tiene el conjunto de roles/permisos requerido |
| Un administrador modifica datos dentro de su propio tenant | Permitido solo dentro del scope del tenant y solo cuando IsManagementOwner = true |
| Un administrador intenta modificar datos de otro tenant | Rechazado aunque tenga acceso al portal |
| Un cliente de API externa se autentica | Usa el flujo de resolucion IDP del tenant y el grafo de autorizacion |
| Auditoria | Toda mutacion interna de tenant debe registrarse |
Branch: Representa una unidad de ubicacion fisica o logica. Provee un ambito geográfico u organizacional. Aplica reglas de geocercado y habilita delegación de administración. Branding: Contiene la configuración de identidad visual (logo, colores, textos) y dominio personalizado con verificación DNS. Controla cómo se renderiza el portal de login. IdentityProvider: Representa un proveedor de autenticación externo (OIDC, SAML2, WS_FED). Registra la intención estratégica y el contrato a nivel de negocio para el tenant.
Tenant es su propio aggregate root. Todas las mutaciones de Branch, Branding e IdentityProvider pasan por comandos de Tenant.
Code debe ser globalmente unico en todo el sistema.Tenant con TenantStatus = Suspended bloquea todos los flujos de autenticacion de sus usuarios.IdpStrategy debe ser coherente: si es FEDERATED, debe existir al menos un IdentityProvider activo.ParentTenantId != null) hereda politicas del tenant padre.Code debe ser unico dentro del Tenant propietario.Branch no puede ser eliminada si existen registros activos de UserAccount o Profile asociados.GeofencingMetadata debe ser JSON valido cuando se proporciona.Branding por Tenant (relacion 1:1).CustomDomain debe ser un hostname valido cuando se proporciona.DnsVerificationStatus comienza en PENDING cuando se establece CustomDomain y no puede establecerse manualmente a VERIFIED (solo por el servicio de verificacion DNS).LogoFormat debe coincidir con el formato real del URI de Logo subido.Code debe ser unico dentro del Tenant propietario.IdentityProvider debe ser desactivado antes de ser eliminado.IdentityProvider que es el unico IdP activo para un tenant Federado no esta permitido a menos que se cambie primero el IdpStrategy.Strategy no puede cambiarse despues del registro — es inmutable una vez establecida.IsManagementOwner identifica los tenants que pueden administrar su propio scope interno de UMS sin depender obligatoriamente del flujo IDP de la API externa.| Entidad / VO | Tipo | Notas |
|—|—|—|
| Code | Value Object | Identificador unico global del tenant |
| Name | Value Object | Nombre para mostrar |
| OrganizationType | Enum | COMPANY · DIVISION · BRANCH_OFFICE |
| IdpStrategy | Enum | LOCAL · FEDERATED · HYBRID |
| IsManagementOwner | Flag | Identifica el tenant responsable de la gestion interna de UMS |
| CompanyReference | Value Object | Referencia al sistema ERP (nullable) |
| TenantStatus | Enum | Active · Suspended · Inactive |
| AuditValueObject | Value Object | CreatedAt/By, UpdatedAt/By |
| Evento | Disparador |
|—|—|
| TenantCreatedEvent | Nuevo tenant registrado |
| TenantSuspendedEvent | Tenant suspendido por admin de plataforma |
| TenantActivatedEvent | Tenant reactivado |
| BranchCreatedEvent | Nueva rama agregada al tenant |
| BranchDeactivatedEvent | Rama desactivada |
| BranchReactivatedEvent | Rama reactivada |
| BranchRemovedEvent | Rama eliminada definitivamente |
| BrandingCreatedEvent | Branding configurado por primera vez |
| BrandingUpdatedEvent | Atributos de branding actualizados |
| BrandingRemovedEvent | Configuracion de branding eliminada |
| BrandingDnsVerifiedEvent | Dominio personalizado verificado por DNS |
| BrandingDnsFailedEvent | Intento de verificacion DNS fallido |
| IdentityProviderRegisteredEvent | Nuevo IdP registrado |
| IdentityProviderActivatedEvent | IdP activado |
| IdentityProviderDeactivatedEvent | IdP desactivado |
| IdentityProviderRemovedEvent | IdP eliminado definitivamente |
| Comando | Descripcion |
|—|—|
| RegisterTenantCommand | Crear un nuevo tenant |
| SuspendTenantCommand | Suspender un tenant activo |
| ActivateTenantCommand | Reactivar un tenant suspendido |
| AddBranchCommand | Agregar una rama al tenant |
| UpdateBranchCommand | Actualizar nombre o metadatos de geocercado |
| DeactivateBranchCommand | Desactivar una rama |
| ReactivateBranchCommand | Reactivar una rama |
| RemoveBranchCommand | Eliminar una rama sin dependientes |
| ConfigureBrandingCommand | Configurar branding por primera vez |
| UpdateBrandingCommand | Actualizar atributos de branding |
| SetCustomDomainCommand | Agregar o reemplazar el dominio personalizado |
| RemoveBrandingCommand | Eliminar la configuracion de branding |
| MarkDnsVerifiedCommand | Interno — llamado por el servicio de verificacion DNS |
| MarkDnsFailedCommand | Interno — llamado por el servicio de verificacion DNS |
| RegisterIdentityProviderCommand | Registrar un IdP externo |
| ActivateIdentityProviderCommand | Activar un IdP |
| DeactivateIdentityProviderCommand | Desactivar un IdP |
| RemoveIdentityProviderCommand | Eliminar definitivamente un IdP inactivo |
ITenantRepository.IBranchDependencyChecker — servicio de dominio para validar dependencias antes de eliminar rama.IIdpStrategyConsistencyService — valida que la desactivacion de un IdP no deje al tenant sin ruta de autenticacion.Tenant (Aggregate Root)
├── Props: TenantProps
│ ├── Id: IdValueObject
│ ├── Code: Code
│ ├── Name: Name
│ ├── OrganizationType: OrganizationType
│ ├── IdpStrategy: IdpStrategy
│ ├── IsManagementOwner: bool
│ ├── CompanyReference?: CompanyReference
│ ├── ParentTenantId?: TenantId
│ ├── Status: TenantStatus
│ └── Audit: AuditValueObject
├── Branch (Entidad Propia, 0..N)
│ └── Props: BranchProps
│ ├── Id: IdValueObject
│ ├── TenantId: TenantId
│ ├── Code: Code
│ ├── Name: Name
│ ├── GeofencingMetadata?: Value (JSON)
│ ├── IsActive: bool
│ └── Audit: AuditValueObject
├── Branding (Entidad Propia, 0..1)
│ └── Props: BrandingProps
│ ├── Id: IdValueObject
│ ├── TenantId: TenantId
│ ├── Logo: Logo
│ ├── LogoFormat: LogoFormat
│ ├── PrimaryColor: HexColor
│ ├── BackgroundStyle: BackgroundStyle
│ ├── HeadlineText: LoginText
│ ├── SecondaryText: LoginText
│ ├── PrimaryButtonLabel: LoginText
│ ├── FooterText: LoginText
│ ├── CustomDomain?: CustomDomain
│ ├── DnsVerificationStatus: DnsVerificationStatus
│ ├── DnsCnameTarget: DnsCnameTarget
│ ├── MagicLinkFallbackEnabled: bool
│ └── Audit: AuditValueObject
└── IdentityProvider (Entidad Propia, 0..N)
└── Props: IdentityProviderProps
├── Id: IdValueObject
├── TenantId: TenantId
├── Code: Code
├── Name: Name
├── Description: Description
├── Strategy: IdpStrategy
├── IsActive: bool
└── Audit: AuditValueObject
Tenant:
Active ──► Suspended ──► Active
Active ──► Inactive (terminal)
Branch:
Activo (IsActive = true) ──► Desactivado (IsActive = false) ──► Activo
└──► Eliminado (si no tiene dependientes)
Branding (DNS):
(CustomDomain establecido) -> DnsVerificationStatus = Pending
├──► Verified (CNAME DNS coincide)
└──► Failed (CNAME faltante o incorrecto)
└──► Pending (en reintento)
IdentityProvider:
Registrado (IsActive = false) ──► Activado (IsActive = true) ──► Desactivado ──► Eliminado
sequenceDiagram
participant C as Cliente
participant H as RegisterTenantHandler
participant R as ITenantRepository
C->>H: RegisterTenantCommand(code, name, orgType, idpStrategy, createdBy)
H->>R: ExistsByCode(code)
R-->>H: false
H->>H: Crear Tenant (Status = Active)
H->>H: Emitir TenantCreatedEvent
H->>R: Add(tenant)
H-->>C: tenantId
sequenceDiagram
participant C as Cliente
participant H as SuspendTenantHandler
participant R as ITenantRepository
participant T as Tenant (AR)
C->>H: SuspendTenantCommand(tenantId, actorId)
H->>R: GetById(tenantId)
R-->>H: Tenant
H->>T: tenant.Suspend(actorId)
T->>T: Guardia: Status debe ser Active
T->>T: Status = Suspended
T->>T: Emitir TenantSuspendedEvent
H->>R: Update(tenant)
H-->>C: void
sequenceDiagram
participant C as Cliente
participant H as AddBranchHandler
participant R as ITenantRepository
participant T as Tenant (AR)
C->>H: AddBranchCommand(tenantId, code, name, geofencing?)
H->>R: GetById(tenantId)
R-->>H: Tenant
H->>T: tenant.AddBranch(branchId, code, name, geofencing, createdBy)
T->>T: Guardia: code unico dentro del tenant
T->>T: Guardia: tenant activo
T->>T: Crear Branch (IsActive = true)
T->>T: Emitir BranchCreatedEvent
H->>R: Update(tenant)
H-->>C: BranchId
sequenceDiagram
participant C as Cliente
participant H as RemoveBranchHandler
participant R as ITenantRepository
participant T as Tenant (AR)
participant D as IBranchDependencyChecker
C->>H: RemoveBranchCommand(tenantId, branchId, actorId)
H->>D: HasDependents(branchId)
D-->>H: false
H->>R: GetById(tenantId)
R-->>H: Tenant
H->>T: tenant.RemoveBranch(branchId, actorId)
T->>T: Eliminar Branch de coleccion
T->>T: Emitir BranchRemovedEvent
H->>R: Update(tenant)
H-->>C: void
sequenceDiagram
participant C as Cliente
participant H as ConfigureBrandingHandler
participant R as ITenantRepository
participant T as Tenant (AR)
participant DNS as IDnsVerificationService
C->>H: ConfigureBrandingCommand(tenantId, logo, colores, textos, customDomain?)
H->>R: GetById(tenantId)
R-->>H: Tenant
H->>T: tenant.ConfigureBranding(props, createdBy)
T->>T: Guardia: Branding no debe existir
T->>T: Crear Branding (DnsVerificationStatus = Pending si customDomain)
T->>T: Emitir BrandingCreatedEvent
H->>R: Update(tenant)
alt customDomain proporcionado
H->>DNS: ScheduleVerification(tenantId, cnameTarget, customDomain)
end
H-->>C: BrandingId
sequenceDiagram
participant DNS as IDnsVerificationService
participant H as MarkDnsVerifiedHandler
participant R as ITenantRepository
participant T as Tenant (AR)
DNS->>H: MarkDnsVerifiedCommand(tenantId, brandingId)
H->>R: GetById(tenantId)
R-->>H: Tenant
H->>T: tenant.MarkDnsVerified(brandingId)
T->>T: Branding.DnsVerificationStatus = Verified
T->>T: Emitir BrandingDnsVerifiedEvent
H->>R: Update(tenant)
DNS-->>H: ok
sequenceDiagram
participant C as Cliente
participant H as RegisterIdpHandler
participant R as ITenantRepository
participant T as Tenant (AR)
C->>H: RegisterIdentityProviderCommand(tenantId, code, name, description, strategy, createdBy)
H->>R: GetById(tenantId)
R-->>H: Tenant
H->>T: tenant.RegisterIdentityProvider(idpId, code, name, description, strategy, createdBy)
T->>T: Guardia: code unico dentro del tenant
T->>T: Crear IdentityProvider (IsActive = false)
T->>T: Emitir IdentityProviderRegisteredEvent
H->>R: Update(tenant)
H-->>C: IdpId
sequenceDiagram
participant C as Cliente
participant H as ActivateIdpHandler
participant R as ITenantRepository
participant T as Tenant (AR)
participant SVC as IIdpStrategyConsistencyService
C->>H: ActivateIdentityProviderCommand(tenantId, idpId, actorId)
H->>SVC: ValidateActivation(tenantId, idpId)
SVC-->>H: valido
H->>R: GetById(tenantId)
R-->>H: Tenant
H->>T: tenant.ActivateIdentityProvider(idpId, actorId)
T->>T: Buscar IdP en coleccion
T->>T: IdP.IsActive = true
T->>T: Emitir IdentityProviderActivatedEvent
H->>R: Update(tenant)
H-->>C: void
erDiagram
TENANT ||--o{ BRANCH : "opera"
TENANT ||--o| BRANDING : "configura"
TENANT ||--o{ IDENTITY_PROVIDER : "registra"
TENANT ||--o{ USER_ACCOUNT : "tiene"
TENANT |o--o{ TENANT : "es_padre_de"
BRANCH ||--o{ USER_ACCOUNT : "alcance"
BRANCH ||--o{ PROFILE : "contexto_para"
IDENTITY_PROVIDER ||--o{ IDP_CONFIGURATION : "configured_by"
TENANT {
uniqueidentifier TenantId PK
nvarchar Code "Unico global"
nvarchar Name
nvarchar OrganizationType "COMPANY-DIVISION-BRANCH_OFFICE"
nvarchar IdpStrategy "LOCAL-FEDERATED-HYBRID"
nvarchar CompanyReference "Nullable"
uniqueidentifier ParentTenantId "Nullable FK"
nvarchar Status "ACTIVE-SUSPENDED-INACTIVE"
datetime2 CreatedAt
uniqueidentifier CreatedBy
datetime2 UpdatedAt
uniqueidentifier UpdatedBy
}
BRANCH {
uniqueidentifier BranchId PK
uniqueidentifier TenantId FK
nvarchar Code "Unico por TenantId"
nvarchar Name
nvarchar GeofencingMetadata "JSON Nullable"
bit IsActive
datetime2 CreatedAt
uniqueidentifier CreatedBy
datetime2 UpdatedAt
uniqueidentifier UpdatedBy
}
BRANDING {
uniqueidentifier BrandingId PK
uniqueidentifier TenantId FK "Unico - 1:1"
nvarchar Logo "URI Storage Path"
nvarchar LogoFormat "PNG-SVG-JPEG"
nvarchar PrimaryColor "Hex Color"
nvarchar BackgroundStyle "Glassmorphism-SleekDark"
nvarchar HeadlineText
nvarchar SecondaryText
nvarchar PrimaryButtonLabel
nvarchar FooterText
nvarchar CustomDomain "FQDN Nullable"
nvarchar DnsVerificationStatus "PENDING-VERIFIED-FAILED"
nvarchar DnsCnameTarget "CNAME de Plataforma"
bit MagicLinkFallbackEnabled
datetime2 CreatedAt
uniqueidentifier CreatedBy
datetime2 UpdatedAt
uniqueidentifier UpdatedBy
}
IDENTITY_PROVIDER {
uniqueidentifier IdpId PK
uniqueidentifier TenantId FK
nvarchar Code "Unico por TenantId"
nvarchar Name
nvarchar Description
nvarchar Strategy "OIDC-SAML2-WS_FED"
bit IsActive
datetime2 CreatedAt
uniqueidentifier CreatedBy
datetime2 UpdatedAt
uniqueidentifier UpdatedBy
}
flowchart TD
subgraph Identity["Identity BC"]
T[Tenant AR]
B[Branch Entity]
BR[Branding Entity]
IDP[IdentityProvider Entity]
UA[UserAccount AR]
T --> B
T --> BR
T --> IDP
UA -->|TenantId| T
UA -->|BranchId opcional| B
end
subgraph Authorization["Authorization BC"]
PROF[Profile AR]
PROF -->|BranchId alcance opcional| B
end
subgraph Configuration["Configuration BC"]
IDPC[IdpConfiguration AR]
end
subgraph Infrastructure["Infrastructure"]
DNS[DNS Verification Service]
STORE[File Storage - Logo URI]
EXTIDP[IdP Externo - Azure AD, Okta]
end
subgraph Audit["Audit BC"]
AUD[AuditRecord]
end
IDP -->|IdentityProviderRegisteredEvent| IDPC
IDP -->|IdentityProviderDeactivatedEvent| IDPC
EXTIDP -->|Contrato de Protocolo| IDP
DNS -->|MarkDnsVerifiedCommand| BR
DNS -->|MarkDnsFailedCommand| BR
STORE -->|Logo URI almacenado| BR
T -->|eventos de dominio| AUD
B -->|eventos de dominio| AUD
BR -->|eventos de dominio| AUD
IDP -->|eventos de dominio| AUD
| Comando | Entrada | Salida |
|—|—|—|
| RegisterTenantCommand | code, name, orgType, idpStrategy, createdBy | Guid tenantId |
| SuspendTenantCommand | tenantId, actorId | void |
| ActivateTenantCommand | tenantId, actorId | void |
| AddBranchCommand | tenantId, code, name, geofencingMetadata?, createdBy | Guid branchId |
| UpdateBranchCommand | tenantId, branchId, name?, geofencingMetadata?, updatedBy | void |
| DeactivateBranchCommand | tenantId, branchId, actorId | void |
| ReactivateBranchCommand | tenantId, branchId, actorId | void |
| RemoveBranchCommand | tenantId, branchId, actorId | void |
| ConfigureBrandingCommand | tenantId, logo, logoFormat, primaryColor, backgroundStyle, headlineText, secondaryText, primaryButtonLabel, footerText, customDomain?, cnameTarget, magicLinkFallback, createdBy | Guid brandingId |
| UpdateBrandingCommand | tenantId, brandingId, campos..., updatedBy | void |
| SetCustomDomainCommand | tenantId, brandingId, customDomain, updatedBy | void |
| RemoveBrandingCommand | tenantId, brandingId, actorId | void |
| MarkDnsVerifiedCommand | tenantId, brandingId | void |
| MarkDnsFailedCommand | tenantId, brandingId, reason | void |
| RegisterIdentityProviderCommand | tenantId, code, name, description, strategy, createdBy | Guid idpId |
| ActivateIdentityProviderCommand | tenantId, idpId, actorId | void |
| DeactivateIdentityProviderCommand | tenantId, idpId, actorId | void |
| RemoveIdentityProviderCommand | tenantId, idpId, actorId | void |
| Consulta | Retorna |
|—|—|
| GetTenantByIdQuery(tenantId) | TenantDto? |
| GetTenantByCodeQuery(code) | TenantDto? |
| Codigo | Condicion |
|—|—|
| TENANT_CODE_DUPLICATE | Code ya existe globalmente |
| TENANT_NOT_FOUND | tenantId desconocido |
| TENANT_NOT_ACTIVE | Operacion requiere tenant activo |
| TENANT_SUSPENDED | Tenant actualmente suspendido |
| BRANCH_CODE_DUPLICATE | Code ya existe en el tenant |
| BRANCH_NOT_FOUND | branchId desconocido en el tenant |
| BRANCH_HAS_DEPENDENTS | Eliminacion bloqueada por usuarios o perfiles activos |
| BRANCH_ALREADY_INACTIVE | Desactivar una rama ya inactiva |
| BRANDING_ALREADY_EXISTS | ConfigureBranding llamado dos veces |
| BRANDING_NOT_FOUND | Sin branding configurado para el tenant |
| DNS_ALREADY_VERIFIED | Intento de re-verificar un dominio ya verificado |
| INVALID_CUSTOM_DOMAIN | No es un formato FQDN valido |
| IDP_CODE_DUPLICATE | Code existe en el tenant |
| IDP_NOT_FOUND | idpId desconocido en el tenant |
| IDP_STRATEGY_IMMUTABLE | Intento de cambiar Strategy |
| IDP_SOLE_ACTIVE_PROVIDER | Desactivacion dejaria al tenant sin autenticacion |
| IDP_NOT_INACTIVE | Eliminacion intentada en IdP activo |
| Indice | Columnas | Tipo |
|—|—|—|
| IX_Tenant_Code | Code | Unico |
| IX_Tenant_ParentTenantId | ParentTenantId | No unico |
| IX_Branch_TenantId | TenantId | No unico |
| IX_Branch_TenantId_Code | TenantId, Code | Unico |
| IX_Branch_IsActive | IsActive | No unico |
| IX_Branding_TenantId | TenantId | Unico (impone 1:1) |
| IX_Branding_CustomDomain | CustomDomain | Unico (parcial - no nulo) |
| IX_IdentityProvider_TenantId_Code | TenantId, Code | Unico |
| IX_IdentityProvider_TenantId_IsActive | TenantId, IsActive | No unico |
TenantId.Code en Tenant es clave unica global — no por tenant.CustomDomain unico entre todos los tenants (un dominio no puede ser reclamado por dos tenants).| Operacion | Rol Requerido | |—|—| | Registrar Tenant | Platform:Admin | | Suspender / Activar Tenant | Platform:Admin | | Agregar / Eliminar Branch | Tenant:Admin | | Desactivar / Reactivar Branch | Tenant:Admin | | Listar Branches | Tenant:Admin · Tenant:UserManager | | Configurar / Actualizar Branding | Tenant:Admin | | Establecer Dominio Personalizado | Tenant:Admin | | Marcar DNS Verificado/Fallido | Solo servicio interno | | Registrar / Eliminar IdP | Tenant:Admin | | Activar / Desactivar IdP | Tenant:Admin |
TENANT_CREATED, TENANT_SUSPENDED, TENANT_ACTIVATEDBRANCH_CREATED, BRANCH_DEACTIVATED, BRANCH_REACTIVATED, BRANCH_REMOVEDBRANDING_CONFIGURED, BRANDING_UPDATED, BRANDING_REMOVED, DNS_VERIFIED, DNS_FAILEDIDP_REGISTERED, IDP_ACTIVATED, IDP_DEACTIVATED, IDP_REMOVEDIdentityProvider en si no almacena credenciales. Los secretos viven en IDP_CONFIGURATION.SecretRef (ruta al vault).El UMS soporta un tenant especial llamado INTERNAL_ADMIN (ID: 11111111-1111-1111-1111-111111111111) que está reservado para administradores internos de la plataforma. Los usuarios pertenecientes a este tenant tienen privilegios elevados que les permiten:
| Modo | Descripción | Aislamiento |
|---|---|---|
| Acceso por Tenant (Regular) | Los usuarios solo pueden acceder a datos de su propio tenant | Aislamiento completo via query filters |
| Acceso Admin Interno | Los usuarios en tenant INTERNAL_ADMIN pueden acceder a múltiples tenants |
Acceso cross-tenant via ITenantContext.EnableCrossTenantAccess() |
INTERNAL_ADMIN, el sistema establece isInternalAdmin=true en los claims del JWTis_internal_admin se añade al tokenITenantContext.Initialize() se llama con isInternalAdmin=truePOST /api/v1/auth/switch-tenant para:
{ "tenantId": "...", "enableCrossTenantAccess": false }{ "tenantId": "...", "enableCrossTenantAccess": true }public interface ITenantContext
{
Guid? OrganizationId { get; }
Guid? OriginalTenantId { get; }
bool IsInternalAdmin { get; }
void Initialize(Guid userTenantId, bool isInternalAdmin);
void EnableCrossTenantAccess(); // Establece OrganizationId = null (bypasses filters)
void DisableCrossTenantAccess(); // Resetea al tenant original del usuario
void SetOrganizationId(Guid organizationId);
}
| Método | Endpoint | Descripción |
|---|---|---|
| POST | /api/v1/auth/login |
Retorna flag isInternalAdmin en la respuesta |
| POST | /api/v1/auth/switch-tenant |
Cambiar contexto de tenant (solo admins) |
| GET | /api/v1/auth/session |
Retorna sesión actual con flag de admin |
OrganizationId — enforced en TenantContext.SetOrganizationId()INTERNAL_ADMIN pueden llamar al endpoint switch-tenantAuditRecordOrganizationId es null (muestra todos los tenants a admins)Consulta GraphQL (Recomendado)
El endpoint GraphQL tenantBranches(tenantId: UUID!) es la forma recomendada para acceder a datos de branches de cualquier tenant. Los admins internos pueden consultar branches de cualquier tenant pasando el parámetro tenantId directamente. Este enfoque funciona correctamente sin necesidad de cambiar el contexto de tenant.
Endpoint REST /api/v1/auth/switch-tenant
Authentication:Enabled puede estar en falseITenantContext desde los claims del JWT después de la validaciónis_internal_admin=trueQuery Splitting de EF Core (SQLite)
EF Core 7+ usa split query mode por defecto cuando se usan múltiples sentencias Include(). Esto causa SQLite Error: 'near "EXEC": syntax error' porque las split queries usan sentencias EXEC no soportadas por SQLite. Usar .AsSingleQuery() para forzar modo single-query:
var record = await dbContext.Tenants
.AsSingleQuery() // Fuerza single query en lugar de split
.Include(x => x.Branches)
.Include(x => x.IdentityProviders)
.Include(x => x.Branding)
.FirstOrDefaultAsync(x => x.Id == id);
| Componente | Estado | Notas |
|---|---|---|
Login con INTERNAL_ADMIN |
Funcionando | Retorna isInternalAdmin: true en la respuesta |
Query GraphQL tenantBranches |
Funcionando | El admin puede consultar branches de cualquier tenant |
Endpoint REST switch-tenant |
Funcionando (con fix) | JWT validado directamente, TenantContext inicializado manualmente |
Query GraphQL getTenants |
No disponible | Usar endpoint REST u otra consulta alternativa |
| CRUD de Branch via GraphQL | Funcionando | Modo single query previene problemas con SQLite |
Los administradores con permisos apropiados pueden restablecer contraseñas de usuarios y modificar períodos de vigencia de cuentas de usuario. El alcance de estas operaciones está determinado por el tipo de administrador y su asociación de tenant.
| Tipo de Admin | Asociación de Tenant | Alcance Operativo |
|---|---|---|
| Admin de Plataforma Interno | Pertenece al tenant INTERNAL_ADMIN |
Puede operar sobre usuarios en cualquier tenant |
| Admin de Tenant | Pertenece a un tenant específico | Puede operar sobre usuarios en su propio tenant únicamente |
CAN_RESET_PASSWORD asignado a su rolCAN_MODIFY_VALIDITY_PERIOD asignado a su rolCAN_RESET_PASSWORD y/o CAN_MODIFY_VALIDITY_PERIOD)targetUser.TenantId == OrganizationId)AUTH_010Estas capacidades son controladas por feature flags (configurables a nivel sistema o tenant):
| Feature Flag | Descripción | Default |
|---|---|---|
ALLOW_PASSWORD_RESET_BY_ADMIN |
Habilita el reset de contraseña por admin | true |
ALLOW_VALIDITY_PERIOD_MODIFICATION |
Habilita la modificación de período de vigencia | true |
Cada reset de contraseña y modificación de período de vigencia DEBE generar un registro de auditoría con:
| Campo | Descripción |
|---|---|
adminUserId |
ID del administrador que realizó la acción |
targetUserId |
ID del usuario afectado por la acción |
targetTenantId |
ID del tenant del usuario afectado |
timestamp |
Fecha y hora de la acción (UTC) |
operationType |
PASSWORD_RESET o VALIDITY_PERIOD_MODIFIED |
previousExpiresAt |
Expiración de vigencia anterior (para cambios de vigencia) |
newExpiresAt |
Nueva expiración de vigencia (para cambios de vigencia) |
reason |
Razón proporcionada por el administrador para la acción |
| Método | Endpoint | Descripción | Alcance |
|---|---|---|---|
| POST | /user-accounts/{userAccountId}/passwords/reset |
Restablecer contraseña para usuario objetivo | Basado en tipo de admin |
| PATCH | /user-accounts/{userAccountId}/validity |
Modificar período de vigencia del usuario | Basado en tipo de admin |
| Código | Descripción |
|---|---|
AUTH_009 |
Administrador carece del permiso requerido |
AUTH_010 |
Usuario objetivo fuera del alcance del administrador |
USER_015 |
Usuario federado no puede tener contraseña local restablecida |
CONFIG_003 |
Período de vigencia solicitado excede el máximo permitido |
| Parámetro | Ubicación de Config | Default | Descripción |
|---|---|---|---|
MAX_VALIDITY_PERIOD_DAYS |
AppConfiguration |
365 | Período de vigencia máximo permitido |
MIN_PASSWORD_LENGTH |
AppConfiguration |
12 | Requisito de longitud mínima de contraseña |
PASSWORD_RESET_NOTIFICATION_CHANNEL |
AppConfiguration |
Canal para notificar a usuarios |
UMS proporciona una capacidad centralizada de gestión de parámetros del sistema a través del aggregate AppConfiguration. Los parámetros pueden tener scope como Global (a nivel de sistema), Tenant (específico de tenant), Suite (específico de system suite), o Module (específico de módulo).
| Scope | Descripción | Quién Puede Gestionar |
|---|---|---|
| Global | Parámetros a nivel de sistema sin asociación de tenant | Solo admins internos |
| Tenant | Parámetros específicos de tenant | Admins internos (cualquier tenant), Admins de tenant (solo su propio tenant) |
| Suite | Parámetros scope a un system suite específico | Solo admins internos |
| Module | Parámetros scope a un módulo específico | Solo admins internos |
IsInternalAdmin=true pueden acceder a configuraciones globales403 Forbidden403 ForbiddenITenantContext.IsInternalAdmin determina si el usuario tiene acceso cross-tenantITenantContext.OrganizationId determina el scope de tenant del usuarioTenantId, SystemSuiteId, y ModuleIdpublic class AppConfiguration : AggregateRoot<AppConfiguration, AppConfigurationProps>
{
public TenantId? TenantId { get; } // Null = global/system-wide
public SystemSuiteId? SystemSuiteId { get; } // Null si no es específico de suite
public IdValueObject? ModuleId { get; } // Null si no es específico de módulo
public Code Code { get; } // Clave única del parámetro
public ConfigurationValue Value { get; } // Valor del parámetro
public Description Description { get; } // Descripción legible
public ConfigurationScope Scope { get; } // Auto-derivado: Global/Tenant/Suite/Module
public bool IsInheritable { get; } // ¿Pueden las configs hijas sobrescribir?
public bool IsEncrypted { get; } // ¿Está el valor encriptado en reposo?
public string Version { get; } // Versionado semántico
public ConfigStatus Status { get; } // Draft/Published/Archived
}
Draft → Published → Archived
↑
└── Solo puede ser modificado en estado Draft
| Hardcode Actual | Código de Parámetro | Valor Default |
|---|---|---|
Frontend ACCESS_TOKEN_DURATION |
ACCESS_TOKEN_DURATION_MS |
3600000 (1 hora) |
Frontend REFRESH_TOKEN_DURATION |
REFRESH_TOKEN_DURATION_MS |
604800000 (7 días) |
Constante MIN_PASSWORD_LENGTH |
MIN_PASSWORD_LENGTH |
12 |
Constante MAX_VALIDITY_PERIOD_DAYS |
MAX_VALIDITY_PERIOD_DAYS |
365 |
| Método | Endpoint | Descripción | Autorización |
|---|---|---|---|
| GET | /app-configurations |
Listar configuraciones (filtradas por scope y permisos) | Basado en scope |
| GET | /app-configurations/{id} |
Obtener una configuración | Basado en scope y propiedad |
| POST | /app-configurations |
Crear nueva configuración | Admin interno o admin de tenant (su propio tenant) |
| PUT | /app-configurations/{id} |
Actualizar configuración draft | Igual que crear |
| POST | /app-configurations/{id}/publish |
Publicar configuración | Igual que crear |
| POST | /app-configurations/{id}/archive |
Archivar configuración | Igual que crear |
| Tipo de Evento | Descripción |
|---|---|
APP_CONFIG_CREATED |
Nueva configuración creada |
APP_CONFIG_UPDATED |
Valor o descripción de configuración modificada |
APP_CONFIG_PUBLISHED |
Configuración movida de Draft a Published |
APP_CONFIG_ARCHIVED |
Configuración archivada |
| Flag | Descripción | Default |
|---|---|---|
ALLOW_GLOBAL_CONFIG_MANAGEMENT |
Habilitar gestión de configuración global | true |
ALLOW_TENANT_CONFIG_MANAGEMENT |
Habilitar gestión de configuración de tenant | true |
IsInheritable=true pueden ser sobrescritos por scopes hijos