fix(security): hide the Assistant Admin app and tables from non-admins in meta-data

adminRules() set only the custom checker. The enricher then copied the host's
default level (NOT_PROTECTED on kof22) into it, and PermissionsHelper's
meta-data path returns ALLOW for NOT_PROTECTED without calling the checker,
so every user saw the Assistant Admin app, its tables and the retention
process in the nav (actions on them were still denied). The rules are now
HAS_ACCESS_PERMISSION with deny behavior HIDDEN, plus the checker.

Found while adopting the QBit in kof22 Website-Backend. Regression test runs
on an enriched NOT_PROTECTED host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
james.maesandClaude Opus 5.5 committed 2026-10-12 12:13:00 -05:00
1 parent a1400d6f05
commit 5508f4619d
2 files changed
+50 -2

No files matched your search

@@ -36,6 +36,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData;
@@ -209,11 +211,18 @@ public class AssistantMetaDataBuilder
/*******************************************************************************
** assistant.admin, through the custom checker.
** assistant.admin, through the custom checker. The level and deny behavior
** are explicit: left null, the enricher copies the host's default level
** (often NOT_PROTECTED), and the meta-data path (app tree, table list) then
** allows the object without ever calling the checker, so every user would
** see the Assistant Admin app and its tables in the nav.
*******************************************************************************/
public static QPermissionRules adminRules()
{
return (new QPermissionRules().withCustomPermissionChecker(new QCodeReference(AssistantAdminPermissionChecker.class)));
return (new QPermissionRules()
.withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)
.withDenyBehavior(DenyBehavior.HIDDEN)
.withCustomPermissionChecker(new QCodeReference(AssistantAdminPermissionChecker.class)));
}
@@ -26,12 +26,19 @@ import com.kingsrook.qbits.assistant.model.AssistantPolicy;
import com.kingsrook.qbits.assistant.model.AssistantUsageDaily;
import com.kingsrook.qbits.assistant.testing.AssistantTestFixture;
import com.kingsrook.qbits.assistant.testing.BaseAssistantTest;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
@@ -110,6 +117,38 @@ class AssistantQBitProducerTest extends BaseAssistantTest
/*******************************************************************************
** On a host whose default permission level is NOT_PROTECTED (enriched),
** the meta-data path hides the admin app, tables and retention process from
** a user without assistant.admin, and shows them to an admin.
*******************************************************************************/
@Test
void testAdminObjectsHiddenInMetaDataOnUnprotectedHost() throws Exception
{
QInstance host = AssistantTestFixture.build(config);
host.setDefaultPermissionRules(new QPermissionRules().withLevel(PermissionLevel.NOT_PROTECTED).withDenyBehavior(DenyBehavior.DISABLED));
new QInstanceEnricher(host).enrich();
List<MetaDataWithPermissionRules> adminObjects = List.of(host.getApp(AssistantMetaDataBuilder.APP_NAME),
host.getProcess(AssistantMetaDataBuilder.RETENTION_PROCESS_NAME), host.getTable(AssistantPolicy.TABLE_NAME), host.getTable(AssistantConversation.TABLE_NAME));
QContext.init(host, AssistantTestFixture.sessionFor(AssistantTestFixture.EDITOR));
for(MetaDataWithPermissionRules object : adminObjects)
{
assertThat(PermissionsHelper.getPermissionCheckResult(new MetaDataInput(), object)).as(object.getName()).isEqualTo(PermissionCheckResult.DENY_HIDE);
}
QContext.clear();
QContext.init(host, AssistantTestFixture.sessionFor(AssistantTestFixture.ADMIN));
for(MetaDataWithPermissionRules object : adminObjects)
{
assertThat(PermissionsHelper.getPermissionCheckResult(new MetaDataInput(), object)).as(object.getName()).isEqualTo(PermissionCheckResult.ALLOW);
}
QContext.clear();
}
/*******************************************************************************
**
*******************************************************************************/