Tenant model
Moqui 4 has no native multitenancy. Darpan implements tenancy itself: a tenant is a MoquiUserGroup of type UgtDarpanCompany, and tenant-owned rows carry that group’s ID in companyUserGroupId. Every Darpan installation shares one database across its tenants.
Common scoped fields include:
companyUserGroupId(the tenant key)ownerUserIdcreatedByUserId
Active tenant
The active tenant is a server-side user preference,darpan.auth.activeTenantUserGroupId. It is not stored per session or per browser tab.
facade.AuthFacadeServices.save#ActiveTenantsets it, after checking the user is a member of the requested tenant.- On every request and after login, the webroot hook in
MoquiConf.xmlcallsTenantAccessSupport.syncUserContext. That putsactiveTenantUserGroupIdand the caller’s permissions for it into the user context, where services and entity filters read them.
Permission membership
Tenant permission membership is separate from tenant identity. A user can have one active tenant while their permission level for that tenant comes from membership records. The backend entity model includes:tenantUserGroupIduserIdpermissionUserGroupId- effective date fields
Super admin is not a tenant role. It is granted through the
DARPAN_SUPER_ADMIN or ADMIN group and applies across tenants.
That split lets the backend decide whether the current user can view, run, or edit the active tenant’s data without applying one global role everywhere.
Service checks
Facade services check access before listing, opening, saving, running, or deleting scoped records. Client-side filtering is not the security boundary. Backend access helpers are responsible for:- Resolving the active tenant
- Checking view, run, or edit access
- Applying scope filters to entity queries
- Resolving tenant-scoped temp and output locations
Reading and writing tenant data safely
Two mechanisms keep a read inside the active tenant. Use both the way the existing code does. Entity filters: theDARPAN_ACTIVE_COMPANY_SCOPE entity filter set, in data/SecuritySeedData.xml, adds companyUserGroupId = activeTenantUserGroupId to finds on tenant-owned entities. The set is attached to the facade artifact authorizations, so it applies to entity finds made inside facade service calls while authorization is enabled. A find that disables authorization skips it.
TenantScopedFinder: code that has to read with authorization disabled goes through darpan.facade.common.TenantScopedFinder instead of calling disableAuthz() directly:
One Moqui behaviour to know: when a find specifies the complete primary key,
one() takes the primary-key lookup path, and other conditions on the same find are not applied. A tenant condition next to a full primary key does not restrict the result. Use findOneOwnedByTenant, or compare companyUserGroupId on the returned row.
Tests guard these rules:
DisableAuthzRatchetTestcounts baredisableAuthz()calls in production Groovy outside the allowlisted helpers, and fails the build when the count rises above its baseline.EntityOwnershipGuardTestfails when the same entity is defined in two components, or when core code starts depending on a connector-owned entity.
Shared source configs
An API source config (HotWax OMS, Shopify, or NetSuite) can be shared with other tenants. Sharing links the tenants to one config row; it never copies it.darpan.auth.ConfigTenantAccessholds one grant per peer tenant, keyed by config type, config ID, tenant, and start date.- The owning tenant (the row’s
companyUserGroupId) and every peer tenant use the same row, so an edit made under any member applies to all of them. - Granting and revoking (
facade.ConfigSharingFacadeServices.grant#ConfigTenantAccess/revoke#ConfigTenantAccess) require the caller to be a tenant admin of the target tenant and of a tenant that already uses the config. - Revoking sets
thruDateon the grant. It never deletes the row, so the history of who could reach a credential is kept. list#ConfigTenantAccessreturns the owner, the peers, the member count, and whether the caller may manage the group.
darpan.reconciliation.SftpServerTenantAccess.
Generated file isolation
Generated outputs have database metadata and file-system or object-location data. The backend resolves output paths from active scope so generated files follow the same access model as saved runs and output descriptors. Generated output behavior includes both sides:- The descriptor record users can list, open, or delete
- The scoped file location where the generated data is stored