feat(search): add organization search and private assistant by waleedlatif1 · Pull Request #7477 · simstudioai/sim · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
77abaf3
feat(knowledge): resolve connector tokens through one service-account…
waleedlatif1 Sep 4, 2026
d21f661
feat(knowledge): map Google Drive permissions to document access tokens
waleedlatif1 Sep 4, 2026
a73add1
feat(knowledge): write mirrored document ACLs without re-embedding
waleedlatif1 Sep 4, 2026
4c0e817
feat(knowledge): crawl Google Drive as an administrator and mirror it…
waleedlatif1 Sep 4, 2026
0fd6df2
fix(knowledge): make one email address mean one account
waleedlatif1 Sep 4, 2026
140c772
feat(knowledge): resolve directory groups so mirrored grants reach th…
waleedlatif1 Sep 4, 2026
64053e8
feat(knowledge): let a connector be put into administrator mode
waleedlatif1 Sep 4, 2026
3053ec4
fix(knowledge): gate mirrored access on its own feature, not on Crede…
waleedlatif1 Sep 4, 2026
faab96f
refactor(knowledge): drop directory-group columns nothing reads
waleedlatif1 Sep 4, 2026
972422f
feat(knowledge): mirror Confluence space permissions and page restric…
waleedlatif1 Sep 4, 2026
de993af
feat(knowledge): refresh mirrored directories on their own clock
waleedlatif1 Sep 4, 2026
9e68ad3
fix(knowledge): close two admin-mode gaps found in an architecture audit
waleedlatif1 Sep 4, 2026
8932ca2
fix(knowledge): read shared-drive permissions, and finish a pending s…
waleedlatif1 Sep 4, 2026
7ecdc94
docs(knowledge): state why the ACL ceiling exists rather than where t…
waleedlatif1 Sep 4, 2026
822625b
chore(knowledge): register the directory-sync cron, and collapse the …
waleedlatif1 Sep 4, 2026
8ca4ff1
refactor(auth): one email fold, in SQL and TypeScript, and no reads o…
waleedlatif1 Sep 4, 2026
cc92806
fix(knowledge): Drive field mask named a permission field that does n…
waleedlatif1 Sep 4, 2026
2c86087
fix(knowledge): close the audit findings on administrator access
waleedlatif1 Sep 4, 2026
6f204a0
refactor(knowledge): tighten administrator access after the mechanics…
waleedlatif1 Sep 4, 2026
399149c
refactor(knowledge): collapse the access-mode vocabulary and the dupl…
waleedlatif1 Sep 4, 2026
6e07a0c
Merge remote-tracking branch 'origin/staging' into feat/connector-ser…
Sep 5, 2026
6b9ecfb
Merge remote-tracking branch 'origin/staging' into feat/connector-ser…
Sep 5, 2026
caf82ac
feat(search): unify source access and scale indexing
Sep 5, 2026
30ce06e
fix(ci): align search cleanup checks and chart version
Sep 5, 2026
0689c67
fix(search): clean up source access and directory refresh
waleedlatif1 Sep 5, 2026
cbfe70d
improvement(search): remove Answer with Sim action
waleedlatif1 Sep 5, 2026
9507aa0
Merge remote-tracking branch 'origin/staging' into feat/connector-ser…
Sep 5, 2026
27d7796
improvement(search): simplify source setup and preserve account handoffs
waleedlatif1 Sep 5, 2026
ca68b75
feat(assistant): use canonical search and personal integration accounts
Sep 5, 2026
ee7ab5a
Merge remote-tracking branch 'origin/feat/connector-service-account-a…
Sep 5, 2026
3a3d38a
improvement(search): simplify connection setup and merge staging
waleedlatif1 Sep 5, 2026
93fc4d7
merge: preserve latest assistant and personal account changes
waleedlatif1 Sep 5, 2026
9a1821a
merge: bring in latest staging parsing fixes
waleedlatif1 Sep 5, 2026
a6616d5
Merge remote-tracking branch 'origin/staging' into feat/connector-ser…
Sep 5, 2026
8f687a3
feat(assistant): connect personal accounts in chat and reuse desktop …
Sep 5, 2026
f15777d
Merge remote-tracking branch 'origin/feat/connector-service-account-a…
Sep 5, 2026
b0857a3
fix(slack): keep channel listing independent of credential storage
Sep 5, 2026
c65cc33
improvement(assistant): streamline account checks and bound catalog c…
Sep 5, 2026
febebf4
Merge remote-tracking branch 'origin/staging' into feat/connector-ser…
Sep 5, 2026
066ecb5
improvement(search): unify source setup and member connection actions
waleedlatif1 Sep 5, 2026
4356872
Merge remote-tracking branch 'origin/feat/connector-service-account-a…
waleedlatif1 Sep 5, 2026
fd6fc44
improvement(search): clarify setup requirements and focus account con…
waleedlatif1 Sep 5, 2026
015948a
feat(search): add Gmail, Jira, GitHub and Calendar sources
waleedlatif1 Sep 5, 2026
b939801
Merge remote-tracking branch 'origin/staging' into codex/pr-7477
waleedlatif1 Sep 5, 2026
4770c98
feat(search): document connector setup and harden source access
waleedlatif1 Sep 6, 2026
6e12f9e
Merge remote-tracking branch 'origin/staging' into codex/pr-7477
waleedlatif1 Sep 6, 2026
9810f1f
feat(org): organization surface, app entry, and shared rail chrome
emir-karabeg Sep 6, 2026
987eb2b
fix(search): support GitHub App grants and verify connector ACLs
waleedlatif1 Sep 6, 2026
1bc27a7
fix(search): deny revoked Atlassian grants and bound provider requests
waleedlatif1 Sep 6, 2026
149eb03
Merge remote-tracking branch 'origin/staging' into codex/pr-7477
waleedlatif1 Sep 6, 2026
7e5a792
test(search): verify unchanged document indexing recovery
waleedlatif1 Sep 6, 2026
179d80c
fix(search): harden connector recovery and file lifecycle
waleedlatif1 Sep 6, 2026
90b5bd8
Merge remote-tracking branch 'origin/staging' into codex/pr-7477
waleedlatif1 Sep 6, 2026
4f1e635
feat(org): home composer, page shell, and completion blue
emir-karabeg Sep 7, 2026
c996233
Merge remote-tracking branch 'origin/feat/org' into codex/pr-7477
waleedlatif1 Sep 7, 2026
cc128f1
Merge remote-tracking branch 'origin/staging' into codex/pr-7477
waleedlatif1 Sep 7, 2026
3b63c42
feat(search): integrate organization sources and private assistant
waleedlatif1 Sep 7, 2026
72d067c
Merge remote-tracking branch 'origin/staging' into codex/pr-7477
waleedlatif1 Sep 7, 2026
5f69ee0
fix(org): clarify membership navigation and role-aware settings
waleedlatif1 Sep 7, 2026
b3524ec
Merge remote-tracking branch 'origin/staging' into codex/pr-7477
waleedlatif1 Sep 7, 2026
46fd0cb
fix(ui): align organization and workspace sidebar interactions
waleedlatif1 Sep 7, 2026
715de0d
Merge staging into enterprise organization search
waleedlatif1 Sep 8, 2026
cedf551
feat(org): organization surface UI, unified settings, and Sim Search …
emir-karabeg Sep 8, 2026
9bcb8fd
feat(search): enforce organization integration approvals
Sep 8, 2026
df7b170
feat(connected-accounts): manage shared accounts at organization scop…
TheodoreSpeaks Sep 8, 2026
84b8719
merge: sync organization search with staging
waleedlatif1 Sep 8, 2026
653f23e
Merge remote-tracking branch 'origin/staging' into codex/resolve-pr-7…
waleedlatif1 Sep 8, 2026
73c6cad
Merge remote-tracking branch 'origin/staging' into codex/resolve-pr-7…
waleedlatif1 Sep 8, 2026
73f6b88
fix(search): harden organization setup and document access
waleedlatif1 Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(knowledge): make one email address mean one account
`user.email` is unique byte-for-byte only, so `Alice@corp.com` and
`alice@corp.com` can both exist as separate accounts. Every identity binding in
the product compares the case-folded address, so those two rows are one identity
to it: a credential-group enrollment for either matches both, and each account
receives the `s:` subject token of a managed credential belonging to the other.
That is live on the members-mode path today, and it is the same fold the `u:`
document token depends on.

`user_email_lower_unique` is the constraint that makes the state unreachable —
the standard Postgres form of a case-insensitive unique email, on
`lower(btrim(email))`. Access resolution probes that exact expression, so a
predicate written any other way would silently become a sequential scan of
`user` on every read; verified as an index scan against a real database.

The migration pre-checks for duplicates inside the runner's batch transaction,
so a database that already holds one fails the deploy with a sentence naming the
problem and rolls back having changed nothing. Without that check the concurrent
build would fail on the first duplicate and leave an INVALID index that
`IF NOT EXISTS` skips on every later run — the constraint would appear to exist
while enforcing nothing, which is the one outcome worth engineering against.
Both paths were verified against a real database in rolled-back transactions:
the index builds on current data, and a deliberately inserted case-variant
duplicate raises and rolls back.

Access resolution keeps its own ambiguity check rather than trusting the
constraint to still be there. An index can be dropped during an incident, and a
restore can bring back a database built before it existed; neither should
silently hand one person another's documents. An ambiguous address binds to
nothing, so both accounts keep the tokens every workspace member holds and lose
only what their identity would have granted.

The enrollment join also stops reading `normalized_email`. That column is
declared unique but written by nothing, so the `COALESCE` over it always fell
through to the folded address — and would have silently started matching a
broader set of people the day anything backfilled it.
  • Loading branch information
waleedlatif1 committed Sep 4, 2026
commit 0fd6df20f1133b8afbb5f70523f46bd8131dece8
38 changes: 38 additions & 0 deletions apps/sim/lib/knowledge/access/scope.test.ts
55 changes: 49 additions & 6 deletions apps/sim/lib/knowledge/access/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { db } from '@sim/db'
import { credential, credentialGroup, credentialGroupEnrollment, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { and, eq, inArray, sql } from 'drizzle-orm'
import { and, eq, inArray, type SQL, sql } from 'drizzle-orm'
import type { AnyPgColumn } from 'drizzle-orm/pg-core'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import { LIVE_ENROLLMENT_STATUSES } from '@/lib/credential-groups/credentials'
import { isKnowledgeMemberAccessAvailable } from '@/lib/knowledge/access/availability'
Expand All @@ -23,7 +24,31 @@ export const WORKSPACE_ACCESS_SCOPE: WorkspaceAccessScope = Object.freeze({
tokens: WORKSPACE_ACCESS_TOKENS,
})

/** Enrollment states under which a credential-group membership counts as live. */
/**
* An email address reduced to the identity it names. `user.email` is unique
* byte-for-byte only, so this is the form every binding by email must compare
* — and `user_email_lower_unique` indexes exactly this expression, so a
* predicate written any other way silently becomes a sequential scan.
*/
function foldedEmail(column: AnyPgColumn): SQL<string> {
return sql<string>`lower(btrim(${column}))`
}

/**
* Whether some other account folds to this one's address.
*
* `user_email_lower_unique` is what makes that impossible; this is the
* assertion that access control does not quietly depend on the constraint still
* being there. An index can be dropped during an incident, and a restore can
* bring back a database built before it existed — neither should silently hand
* one person another's documents. One index probe per read is a fair price; it
* plans as an index scan, not a table scan.
*/
const emailHeldByAnotherAccount = sql<boolean>`EXISTS (
SELECT 1 FROM ${user} AS other
WHERE other.id <> ${user.id}
AND lower(btrim(other.email)) = ${foldedEmail(user.email)}
)`

export interface KnowledgeAccessScopeContext {
/** Undefined only for a legacy personal knowledge base, which cannot own connectors. */
Expand Down Expand Up @@ -64,6 +89,7 @@ async function loadUserAccessTokens(

const rows = await db
.select({
emailIsAmbiguous: emailHeldByAnotherAccount,
providerId: credential.providerId,
providerTenantId: credential.providerTenantId,
providerSubjectId: credential.providerSubjectId,
Expand All @@ -72,10 +98,13 @@ async function loadUserAccessTokens(
.leftJoin(
credentialGroupEnrollment,
and(
eq(
credentialGroupEnrollment.email,
sql`COALESCE(${user.normalizedEmail}, lower(btrim(${user.email})))`
),
/**
* The address is folded here rather than read from `normalized_email`,
* which is declared unique but never written: a `COALESCE` over it
* would silently start matching a different, broader set of people the
* day anything backfills that column.
*/
eq(credentialGroupEnrollment.email, foldedEmail(user.email)),
inArray(credentialGroupEnrollment.status, [...LIVE_ENROLLMENT_STATUSES])
)
)
Expand Down Expand Up @@ -103,6 +132,20 @@ async function loadUserAccessTokens(
)
.where(and(eq(user.id, userId), eq(user.emailVerified, true)))

/**
* An address two accounts share identifies neither of them, so it binds to
* nothing. Both accounts keep the tokens every workspace member holds and
* lose only what their identity would have granted — the safe direction, and
* the one that cannot hand one person the other's documents.
*/
if (rows.some((row) => row.emailIsAmbiguous)) {
logger.error('Refusing identity-derived access tokens for an ambiguous email address', {
userId,
workspaceId,
})
return [...WORKSPACE_ACCESS_TOKENS]
}

const subjectTokens = new Set<string>()
for (const row of rows) {
if (!row.providerSubjectId) continue
Expand Down
35 changes: 35 additions & 0 deletions packages/db/migrations/0321_user_email_case_folded_unique.sql
Loading