fix(database): restrict db delete to agent-owning org (system agents admin-only)

- UI: delete button gated by canDeleteDatabases prop threaded from each page
  (org page: only org-created agents; admin page: system agents)
- authz: org path requires agent.organizationId === active org; drop join-based
  attribution so a system agent merely linked to an org is admin-delete-only
This commit is contained in:
charles-gauthereau
2026-07-05 18:31:52 +02:00
parent cd72cfbff4
commit df692c4729
5 changed files with 23 additions and 20 deletions
@@ -81,7 +81,7 @@ export default async function RoutePage(
</PageDescription> </PageDescription>
)} )}
<PageContent className="flex flex-col w-full h-full justify-between gap-6"> <PageContent className="flex flex-col w-full h-full justify-between gap-6">
<AgentContentPage agent={agent} edgeKey={edgeKey} /> <AgentContentPage agent={agent} edgeKey={edgeKey} canDeleteDatabases={true} />
</PageContent> </PageContent>
</Page> </Page>
); );
@@ -94,7 +94,7 @@ export default async function RoutePage(
</PageDescription> </PageDescription>
)} )}
<PageContent className="flex flex-col w-full h-full justify-between gap-6"> <PageContent className="flex flex-col w-full h-full justify-between gap-6">
<AgentContentPage agent={agent} edgeKey={edgeKey}/> <AgentContentPage agent={agent} edgeKey={edgeKey} canDeleteDatabases={!!agent.organizationId}/>
</PageContent> </PageContent>
</Page> </Page>
); );
@@ -23,9 +23,10 @@ import {HealthcheckLog} from "@/db/schema/15_healthcheck-log";
type AgentContentPageProps = { type AgentContentPageProps = {
edgeKey: string; edgeKey: string;
agent: AgentWithDatabases agent: AgentWithDatabases
canDeleteDatabases?: boolean
} }
export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPageProps) => { export const AgentContentPage = ({edgeKey, agent: initialAgent, canDeleteDatabases = false}: AgentContentPageProps) => {
const {data} = useQuery({ const {data} = useQuery({
queryKey: ["agent-data", initialAgent.id], queryKey: ["agent-data", initialAgent.id],
@@ -121,6 +122,7 @@ export const AgentContentPage = ({edgeKey, agent: initialAgent}: AgentContentPag
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)} )}
cardItem={AgentDatabaseCard} cardItem={AgentDatabaseCard}
canDeleteDatabases={canDeleteDatabases}
/> />
</div> </div>
)} )}
@@ -1,23 +1,27 @@
"use client"; "use client";
import {Database} from "@/db/schema/07_database"; import { Database } from "@/db/schema/07_database";
import {DatabaseCard} from "@/components/common/database-card"; import { DatabaseCard } from "@/components/common/database-card";
import {DatabaseDeleteButton} from "@/features/agents/components/database-delete-button"; import { DatabaseDeleteButton } from "@/features/agents/components/database-delete-button";
export type agentDatabaseCardProps = { export type AgentDatabaseCardProps = {
data: Database; data: Database;
canDeleteDatabases?: boolean;
}; };
export const AgentDatabaseCard = (props: agentDatabaseCardProps) => { export const AgentDatabaseCard = (props: AgentDatabaseCardProps) => {
const {data: database} = props; const { data: database, canDeleteDatabases = false } = props;
return ( return (
<DatabaseCard <DatabaseCard
withDetails={false} withDetails={false}
data={database} data={database}
deleteButton={ deleteButton={
database.agentId ? ( canDeleteDatabases && database.agentId ? (
<DatabaseDeleteButton databaseId={database.id} agentId={database.agentId}/> <DatabaseDeleteButton
databaseId={database.id}
agentId={database.agentId}
/>
) : undefined ) : undefined
} }
/> />
@@ -35,11 +35,7 @@ async function assertCanDeleteDatabase(databaseId: string): Promise<void> {
const database = await db.query.database.findFirst({ const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.id, databaseId), where: eq(drizzleDb.schemas.database.id, databaseId),
with: { with: {
agent: { agent: true,
with: {
organizations: true,
},
},
}, },
}); });
@@ -64,10 +60,11 @@ async function assertCanDeleteDatabase(databaseId: string): Promise<void> {
const canManage = activeMember const canManage = activeMember
? computeOrganizationPermissions(activeMember).canManageAgents ? computeOrganizationPermissions(activeMember).canManageAgents
: false; : false;
// Only the organization that CREATED the agent may delete its databases.
// A system agent merely attributed to an org via the join table is
// handled by the isAdmin branch above (agent.organizationId === null).
const hasAccess = const hasAccess =
!!organization && !!organization && agent.organizationId === organization.id;
(agent.organizationId === organization.id ||
agent.organizations.some((o) => o.organizationId === organization.id));
authorized = canManage && hasAccess; authorized = canManage && hasAccess;
} }