Merge branch 'machine-roles' into dev

This commit is contained in:
Owen
2026-10-12 16:25:13 -04:00
18 changed files with 1035 additions and 128 deletions
+83 -110
View File
@@ -1,5 +1,6 @@
import {
Client,
clientOrgRoles,
clients,
clientSiteResources,
clientSiteResourcesAssociationsCache,
@@ -256,9 +257,30 @@ export async function getClientSiteResourceAccess(
eq(clientSiteResources.siteResourceId, siteResource.siteResourceId)
);
const directClientIds = allClientSiteResources.map((row) => row.clientId);
// clients (machines) that get access through a role assigned directly to
// the client instead of through a user
const roleClientIds =
roleIds.length > 0
? await trx
.select({ clientId: clientOrgRoles.clientId })
.from(clientOrgRoles)
.where(
and(
inArray(clientOrgRoles.roleId, roleIds),
eq(clientOrgRoles.orgId, siteResource.orgId)
)
)
.then((rows) => rows.map((row) => row.clientId))
: [];
// Get full client details for directly associated clients
const directClientIds = Array.from(
new Set([
...allClientSiteResources.map((row) => row.clientId),
...roleClientIds
])
);
// Get full client details for directly and client-role associated clients
const directClients =
directClientIds.length > 0
? await trx
@@ -285,7 +307,7 @@ export async function getClientSiteResourceAccess(
const mergedAllClientIds = mergedAllClients.map((c) => c.clientId);
logger.debug(
`rebuildClientAssociations: [getClientSiteResourceAccess] siteResourceId=${siteResource.siteResourceId} mergedClientCount=${mergedAllClientIds.length} clientIds=[${mergedAllClientIds.join(", ")}] (userBased=${newAllClients.length} direct=${directClients.length})`
`rebuildClientAssociations: [getClientSiteResourceAccess] siteResourceId=${siteResource.siteResourceId} mergedClientCount=${mergedAllClientIds.length} clientIds=[${mergedAllClientIds.join(", ")}] (userBased=${newAllClients.length} direct=${directClients.length} clientRoleBased=${roleClientIds.length})`
);
return {
@@ -2149,11 +2171,16 @@ export async function rebuildClientAssociationsFromClient(
}
}
async function rebuildClientAssociationsFromClientImpl(
// Derives the set of site resources a client should have access to from its
// permissions: direct client associations, the owning user's direct and
// role-based associations, and roles assigned directly to the client
// (machine clients). Shared by the rebuild and the cache verification so they
// can't drift apart.
async function getClientSiteResourceIds(
client: Client,
trx: Transaction | typeof db = db
): Promise<void> {
let newSiteResourceIds: number[] = [];
): Promise<number[]> {
const newSiteResourceIds: number[] = [];
// 1. Direct client associations
const directSiteResources = await trx
@@ -2174,7 +2201,20 @@ async function rebuildClientAssociationsFromClientImpl(
...directSiteResources.map((r) => r.siteResourceId)
);
// 2. User-based and role-based access (if client has a userId)
// 2. Roles assigned directly to the client (machine clients). These are
// locked onto this org or else cross-org access could happen
const roleIds = await trx
.select({ roleId: clientOrgRoles.roleId })
.from(clientOrgRoles)
.where(
and(
eq(clientOrgRoles.clientId, client.clientId),
eq(clientOrgRoles.orgId, client.orgId)
)
)
.then((rows) => rows.map((row) => row.roleId));
// 3. User-based and user role-based access (if client has a userId)
if (client.userId) {
// Direct user associations
const userSiteResourceIds = await trx
@@ -2198,8 +2238,7 @@ async function rebuildClientAssociationsFromClientImpl(
...userSiteResourceIds.map((r) => r.siteResourceId)
);
// Role-based access
const roleIds = await trx
const userRoleIds = await trx
.select({ roleId: userOrgRoles.roleId })
.from(userOrgRoles)
.where(
@@ -2210,32 +2249,45 @@ async function rebuildClientAssociationsFromClientImpl(
) // this needs to be locked onto this org or else cross-org access could happen
.then((rows) => rows.map((row) => row.roleId));
if (roleIds.length > 0) {
const roleSiteResourceIds = await trx
.select({ siteResourceId: roleSiteResources.siteResourceId })
.from(roleSiteResources)
.innerJoin(
siteResources,
eq(
siteResources.siteResourceId,
roleSiteResources.siteResourceId
)
)
.where(
and(
inArray(roleSiteResources.roleId, roleIds),
eq(siteResources.orgId, client.orgId) // filter by org to prevent cross-org associations
)
);
roleIds.push(...userRoleIds);
}
newSiteResourceIds.push(
...roleSiteResourceIds.map((r) => r.siteResourceId)
// Role-based access (client roles and user roles)
if (roleIds.length > 0) {
const roleSiteResourceIds = await trx
.select({ siteResourceId: roleSiteResources.siteResourceId })
.from(roleSiteResources)
.innerJoin(
siteResources,
eq(
siteResources.siteResourceId,
roleSiteResources.siteResourceId
)
)
.where(
and(
inArray(
roleSiteResources.roleId,
Array.from(new Set(roleIds))
),
eq(siteResources.orgId, client.orgId) // filter by org to prevent cross-org associations
)
);
}
newSiteResourceIds.push(
...roleSiteResourceIds.map((r) => r.siteResourceId)
);
}
// Remove duplicates
newSiteResourceIds = Array.from(new Set(newSiteResourceIds));
return Array.from(new Set(newSiteResourceIds));
}
async function rebuildClientAssociationsFromClientImpl(
client: Client,
trx: Transaction | typeof db = db
): Promise<void> {
const newSiteResourceIds = await getClientSiteResourceIds(client, trx);
// Get full siteResource details
const newSiteResources =
@@ -2947,86 +2999,7 @@ export async function verifyClientAssociationsCache(
client: Client,
trx: Transaction | typeof db = db
): Promise<ClientAssociationsCacheVerification> {
let newSiteResourceIds: number[] = [];
// 1. Direct client associations
const directSiteResources = await trx
.select({ siteResourceId: clientSiteResources.siteResourceId })
.from(clientSiteResources)
.innerJoin(
siteResources,
eq(siteResources.siteResourceId, clientSiteResources.siteResourceId)
)
.where(
and(
eq(clientSiteResources.clientId, client.clientId),
eq(siteResources.orgId, client.orgId)
)
);
newSiteResourceIds.push(
...directSiteResources.map((r) => r.siteResourceId)
);
// 2. User-based and role-based access (if client has a userId)
if (client.userId) {
const userSiteResourceIds = await trx
.select({ siteResourceId: userSiteResources.siteResourceId })
.from(userSiteResources)
.innerJoin(
siteResources,
eq(
siteResources.siteResourceId,
userSiteResources.siteResourceId
)
)
.where(
and(
eq(userSiteResources.userId, client.userId),
eq(siteResources.orgId, client.orgId)
)
);
newSiteResourceIds.push(
...userSiteResourceIds.map((r) => r.siteResourceId)
);
const roleIds = await trx
.select({ roleId: userOrgRoles.roleId })
.from(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, client.userId),
eq(userOrgRoles.orgId, client.orgId)
)
)
.then((rows) => rows.map((row) => row.roleId));
if (roleIds.length > 0) {
const roleSiteResourceIds = await trx
.select({ siteResourceId: roleSiteResources.siteResourceId })
.from(roleSiteResources)
.innerJoin(
siteResources,
eq(
siteResources.siteResourceId,
roleSiteResources.siteResourceId
)
)
.where(
and(
inArray(roleSiteResources.roleId, roleIds),
eq(siteResources.orgId, client.orgId)
)
);
newSiteResourceIds.push(
...roleSiteResourceIds.map((r) => r.siteResourceId)
);
}
}
newSiteResourceIds = Array.from(new Set(newSiteResourceIds));
const newSiteResourceIds = await getClientSiteResourceIds(client, trx);
const newSiteResources =
newSiteResourceIds.length > 0