Skip to content

Commit 0b893d7

Browse files
authored
feat(atlassian): resolve dynamic scopes and index Confluence attachments (#7935)
* feat(atlassian): resolve dynamic scopes and index Confluence attachments * fix(atlassian): reject mixed All scopes and run permission tests in CI * fix(ci): isolate Confluence migration test database
1 parent 4e49225 commit 0b893d7

48 files changed

Lines changed: 2925 additions & 374 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test-build.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,26 @@ jobs:
245245
lib/knowledge/__integration__/connector-upload.integration.ts
246246
lib/uploads/contexts/organization-logo/application.integration.ts
247247
248+
- name: Verify Confluence identity and directory sync in PostgreSQL
249+
working-directory: apps/sim
250+
env:
251+
KNOWLEDGE_ACL_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_auth_scim
252+
run: >-
253+
bunx vitest run --mode integration
254+
lib/knowledge/__integration__/confluence-identity.integration.ts
255+
lib/knowledge/__integration__/directory-sync.integration.ts
256+
257+
- name: Verify Confluence audience migrations and permission queries in PostgreSQL
258+
working-directory: apps/sim
259+
env:
260+
KNOWLEDGE_ACL_TEST_DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/sim_acl_test
261+
run: |
262+
bun -e 'import postgres from "postgres"; const sql = postgres(process.env.DATABASE_URL); await sql`CREATE DATABASE sim_acl_test`; await sql.end()'
263+
bunx vitest run --mode integration lib/knowledge/access/group-membership.integration.ts
264+
bunx vitest run \
265+
lib/knowledge/access/predicate.postgres.test.ts \
266+
lib/knowledge/connectors/external-directory.postgres.test.ts
267+
248268
test-build:
249269
name: Lint and Test
250270
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-8vcpu-ubuntu-2404' || 'ubuntu-latest' }}

apps/docs/content/docs/knowledgebase/connectors.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Each connector has source-specific fields that control what gets synced. Example
7878

7979
- **Notion** — sync an entire workspace, a specific database, or a single page tree
8080
- **GitHub** — specify a repository, branch, and optional file extension filter
81-
- **Confluence** — enter your Atlassian domain and optionally filter by space key or content type
81+
- **Confluence** — enter your Atlassian domain and choose spaces, or **All** for all spaces accessible at each sync. Optionally filter by content type or label. PDF and Word (`.docx`, Word 97–2003 `.doc`) attachments on matching pages and blog posts are included as separate documents.
8282
- **Azure DevOps** — choose what to sync (wiki pages, work items, repository files, or all), with optional work item type/state filters, a custom WIQL query, and repository/branch/path filters
8383
- **Amazon S3** — point at a bucket with an optional key prefix and a customizable file extension allowlist; S3-compatible stores (Cloudflare R2, MinIO) are supported via a custom endpoint
8484
- **YouTube** — sync a channel (by `@handle` or ID) or playlist, with an optional published-after date filter and the option to exclude Shorts
@@ -88,6 +88,8 @@ Each connector has source-specific fields that control what gets synced. Example
8888

8989
Configuration is validated on save — if a repository doesn't exist or a domain is unreachable, you'll see an error immediately.
9090

91+
Confluence attachment indexing requires `read:attachment:confluence`. For a service account, include it when creating the scoped API token; see the [Confluence scope list](/search/confluence#using-a-service-account). Attachments are checked even when the parent page has not changed. Files over 100 MB appear as skipped; convert Word 6/95 files to `.docx` before attaching them.
92+
9193
</Step>
9294
<Step>
9395

apps/docs/content/docs/search/confluence.mdx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Callout } from 'fumadocs-ui/components/callout'
77
import { Step, Steps } from 'fumadocs-ui/components/steps'
88
import { Image } from '@/components/ui/image'
99

10-
Search pages and blog posts from selected Confluence Cloud spaces. A Sim organization admin enables Confluence; **each teammate connects their own account**.
10+
Search pages, blog posts, and their PDF and Word attachments from selected Confluence Cloud spaces. A Sim organization admin enables Confluence; **each teammate connects their own account**.
1111

1212
| Method | How it works |
1313
| --- | --- |
@@ -38,7 +38,9 @@ Open **Settings → Sources → Add source** and select **Confluence**. This ope
3838

3939
### Choose the account and spaces
4040

41-
Under **Service account**, select a service account or [add one](#using-a-service-account). Enter the same **Confluence site** as the credential, then choose **Spaces**. **All** in the dropdown selects every space the account can currently browse; newly created spaces are not added automatically. Clear the picker search before selecting all.
41+
Under **Service account**, select a service account or [add one](#using-a-service-account). Enter the same **Confluence site** as the credential, then choose **Spaces**. **All** in the dropdown includes every space the syncing account can access at each sync, including newly accessible spaces. Clear the picker search before selecting all.
42+
43+
If you selected all spaces before this behavior was introduced, reselect **All** and save. Previously saved selections remain a fixed list of spaces.
4244

4345
To enter comma-separated keys such as `ENG, PRODUCT`, use the switch beside **Spaces**. Switching between the picker and manual entry keeps your selection.
4446

@@ -63,7 +65,7 @@ After an admin configures Confluence, open **Integrations** and select **Connect
6365
If Confluence is allowed but no source exists, select **Connect** beside Confluence. To add another site later, open the Confluence row’s actions menu (****) and select **Add Confluence site**:
6466

6567
1. Open **Your account** and select a saved account or **Connect Confluence account**. Authorize using the Atlassian email matching your verified Sim email.
66-
2. Enter the hostname under **Atlassian site**, then choose **Spaces**. Use **All** in the dropdown for the complete current list, or the arrows beside **Spaces** to enter comma-separated keys. You can select up to 1,000 spaces in this form.
68+
2. Enter the hostname under **Atlassian site**, then choose **Spaces**. Use **All** in the dropdown for all spaces accessible at each sync, or the arrows beside **Spaces** to enter comma-separated keys. You can select up to 1,000 individual spaces in this form.
6769
3. Select **Connect & Sync**. Sim saves the selected scope and starts indexing with your account.
6870

6971
<Image className="mx-auto h-auto w-full max-w-md"
@@ -87,6 +89,7 @@ Use a **scoped API token** from an Atlassian service account:
8789
read:confluence-content.all
8890
read:page:confluence
8991
read:blogpost:confluence
92+
read:attachment:confluence
9093
read:space:confluence
9194
read:label:confluence
9295
search:confluence
@@ -98,7 +101,7 @@ read:user:confluence
98101
read:group:confluence
99102
```
100103

101-
Use all 12 scopes for account validation, pickers, content, permissions, and directory reads. Central indexing does not need write scopes.
104+
Use all 13 scopes for account validation, pickers, content, attachments, permissions, and directory reads. Central indexing does not need write scopes.
102105

103106
4. Review and create the token, then copy it. Atlassian shows it only once.
104107
5. In Sim's source form, open **Service account → Add service account**. Paste the **API token**, enter **Site domain** (hostname only), and select **Add service account**. Continue in the source form with the same domain.
@@ -122,7 +125,9 @@ See Atlassian's [account setup](https://support.atlassian.com/user-management/do
122125
| **Filter by Label** | Optional comma-separated labels; content can match any listed label. |
123126
| **Metadata tags** | Labels, version, and last-modified tags. |
124127

125-
Search manages the schedule and hides item limits. It indexes published/current content and each page's own text, including supported local callouts and code blocks. Archived content, comments, attachment contents, and expanded Include Page, Excerpt Include, or third-party macro output are excluded. Referenced pages can be indexed separately with their own permissions.
128+
Search manages the schedule and hides item limits. It indexes published/current content and each page's own text, including supported local callouts and code blocks. PDF, Word `.docx`, and Word 97–2003 `.doc` attachments on the selected pages and blog posts are indexed as separate documents with their parent content's permissions. Space, content-type, and label filters apply to the parent content. Attachment changes are checked on each sync, even when the parent text has not changed.
129+
130+
Archived content, comments, other attachment formats, and expanded Include Page, Excerpt Include, or third-party macro output are excluded. Referenced pages can be indexed separately with their own permissions. Attachments over 100 MB are shown as skipped; convert older Word 6/95 files to `.docx` before attaching them.
126131

127132
## Manage access and sync
128133

@@ -146,6 +151,7 @@ In **Sync history**, **Continuing** means a healthy listing needs another batch.
146151
| A new page, blog post, or label is missing | Confluence search can take time to update. Once the content appears in Confluence search with the selected label, sync again. |
147152
| A restricted page is missing | Both your account and the crawling account need access to the page and its ancestors. |
148153
| Embedded content is missing | Index the referenced page separately; remote macro output is excluded. |
154+
| PDF or Word attachments are missing | Check `read:attachment:confluence` and access to the parent page. Existing service-account tokens may need to be replaced with one that includes this scope. Attachment access failures are reported as a partial sync. |
149155
| **Reconnect** or email mismatch | Authorize with the Atlassian account matching your verified Sim email and grant all requested permissions. |
150156

151157
Open a missing page as the affected teammate, check its space and page restrictions, then sync again after correcting access. See Atlassian's [content access](https://support.atlassian.com/confluence-cloud/docs/add-or-remove-page-restrictions/) and [permission inspection](https://support.atlassian.com/confluence-cloud/docs/inspect-a-users-permissions/) guides.

apps/docs/content/docs/search/jira.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ Open **Settings → Sources → Add source** and select **Jira**. This opens **A
3737

3838
### Choose the projects
3939

40-
Under **Account for browsing**, select a saved account (including your personal Search account) or choose **Connect Jira account**. Enter **Jira site**, then choose **Projects**. **All** in the dropdown selects every project the account can currently browse; newly created projects are not added automatically. Clear the picker search before selecting all.
40+
Under **Account for browsing**, select a saved account (including your personal Search account) or choose **Connect Jira account**. Enter **Jira site**, then choose **Projects**. **All** in the dropdown includes every project the syncing account can access at each sync, including newly accessible projects. Clear the picker search before selecting all.
41+
42+
If you selected all projects before this behavior was introduced, reselect **All** and save. Previously saved selections remain a fixed list of projects.
4143

4244
To enter keys such as `ENG, SUPPORT` manually, use the switch beside **Projects**; this works without a browsing account. Switching between the picker and manual entry keeps your selection.
4345

@@ -76,7 +78,7 @@ Each teammate follows this step. Sim reuses an existing authorized account where
7678
After an admin approves Jira, a teammate can select **Connect** on the Jira row in **Integrations**. To add another site or project selection later, open the Jira row's actions menu (****) and select **Add projects**:
7779

7880
1. Open **Your account** and select a saved account or **Connect Jira account**. Authorize using the Atlassian email matching your verified Sim email.
79-
2. Enter the hostname under **Atlassian site**, then choose **Projects**. Clear the picker search to use **All**, or use the arrows beside **Projects** to enter comma-separated keys. You can select up to 1,000 projects. Changing the account or site clears the project selection.
81+
2. Enter the hostname under **Atlassian site**, then choose **Projects**. Clear the picker search to use **All**, or use the arrows beside **Projects** to enter comma-separated keys. You can select up to 1,000 individual projects. Changing the account or site clears the project selection.
8082
3. Select **Connect & Sync**. Sim saves the selected scope and starts indexing with your account.
8183

8284
<Image className="mx-auto h-auto w-full max-w-md"

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.test.tsx

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,68 @@ vi.mock('@/hooks/queries/selectors', () => ({
3434
import { ConnectorSelectorField } from '@/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field'
3535

3636
interface ComboboxCallbacks {
37-
options: { value: string; label: string; hidden?: boolean; onSelect?: () => void }[]
37+
options: {
38+
value: string
39+
label: string
40+
hidden?: boolean
41+
selected?: boolean
42+
onSelect?: () => void
43+
}[]
3844
disabled: boolean
3945
onChange?: (value: string) => void
4046
onMultiSelectChange?: (value: string[]) => void
4147
}
4248

49+
it('stores dynamic All without enumerating a snapshot and lets users select individual items again', async () => {
50+
const field = {
51+
id: 'spaces',
52+
title: 'Spaces',
53+
type: 'selector',
54+
selectorKey: 'confluence.spaces',
55+
multi: true,
56+
allowSelectAll: true,
57+
selectAllValue: '*',
58+
} as const
59+
const root = createRoot(document.createElement('div'))
60+
const render = async (value: string[]) =>
61+
act(async () =>
62+
root.render(
63+
<ConnectorSelectorField
64+
field={field}
65+
value={value}
66+
onChange={mocks.change}
67+
credentialId='credential-1'
68+
sourceConfig={{ domain: 'example.atlassian.net' }}
69+
configFields={[field]}
70+
canonicalModes={{}}
71+
/>
72+
)
73+
)
74+
try {
75+
await render([])
76+
await act(async () =>
77+
mocks.combobox.mock
78+
.lastCall![0].options.find((option) => option.label === 'All')
79+
?.onSelect?.()
80+
)
81+
expect(mocks.change).toHaveBeenLastCalledWith(['*'], [{ id: '*', label: 'All' }])
82+
expect(mocks.loadAll).not.toHaveBeenCalled()
83+
await render(['*'])
84+
await act(async () => mocks.combobox.mock.lastCall![0].onMultiSelectChange?.(['*', 'folder-b']))
85+
expect(mocks.change).toHaveBeenLastCalledWith(
86+
['folder-b'],
87+
[{ id: 'folder-b', label: 'Company docs' }]
88+
)
89+
await render(['*', 'folder-b'])
90+
const all = mocks.combobox.mock.lastCall![0].options.find((option) => option.label === 'All')
91+
await act(async () => all?.onSelect?.())
92+
expect(mocks.change).toHaveBeenLastCalledWith(['*'], [{ id: '*', label: 'All' }])
93+
} finally {
94+
await act(async () => root.unmount())
95+
vi.clearAllMocks()
96+
}
97+
})
98+
4399
beforeEach(() => {
44100
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true)
45101
})

apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-selector-field/connector-selector-field.tsx

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,11 @@ export function ConnectorSelectorField({
139139

140140
const singleValue = Array.isArray(value) ? value[0] : value
141141
const selectedIds = useMemo(
142-
() => (Array.isArray(value) ? value : value ? [value] : []).filter(Boolean),
143-
[value]
142+
() =>
143+
(Array.isArray(value) ? value : value ? [value] : []).filter(
144+
(id) => Boolean(id) && id !== field.selectAllValue
145+
),
146+
[value, field.selectAllValue]
144147
)
145148
const missingSelectedIds = useMemo(() => {
146149
const loadedIds = new Set(options.map((option) => option.id))
@@ -197,6 +200,9 @@ export function ConnectorSelectorField({
197200
}, [options, selectedOptions, searchedOption, selectedLabels, selectedIds])
198201

199202
const handleChange = (nextValue: ConfigFieldValue) => {
203+
if (Array.isArray(nextValue) && field.selectAllValue) {
204+
nextValue = nextValue.filter((id) => id !== field.selectAllValue)
205+
}
200206
bulkGenerationRef.current += 1
201207
setBulkError(null)
202208
const ids = new Set(Array.isArray(nextValue) ? nextValue : nextValue ? [nextValue] : [])
@@ -214,18 +220,26 @@ export function ConnectorSelectorField({
214220

215221
const hasSearch = searchTerm.trim().length > 0 || debouncedSearch.length > 0
216222
const selectedIdSet = new Set(selectedIds)
217-
const allSelected =
218-
!hasMore &&
219-
!truncated &&
220-
options.length > 0 &&
221-
selectedIds.length === options.length &&
222-
options.every((option) => selectedIdSet.has(option.id))
223+
const values = Array.isArray(value) ? value : [value]
224+
const allSelected = field.selectAllValue
225+
? values.length === 1 && values[0] === field.selectAllValue
226+
: !hasMore &&
227+
!truncated &&
228+
options.length > 0 &&
229+
selectedIds.length === options.length &&
230+
options.every((option) => selectedIdSet.has(option.id))
223231
const selectAll = async () => {
224232
if (!isEnabled || hasSearch || isFetching || isLoadingAll) return
225233
if (allSelected) {
226234
handleChange([])
227235
return
228236
}
237+
if (field.selectAllValue) {
238+
bulkGenerationRef.current += 1
239+
setBulkError(null)
240+
onChange([field.selectAllValue], [{ id: field.selectAllValue, label: 'All' }])
241+
return
242+
}
229243
const generation = ++bulkGenerationRef.current
230244
setBulkError(null)
231245
const result = await loadAll()
@@ -265,10 +279,10 @@ export function ConnectorSelectorField({
265279
aria-label={field.title}
266280
multiSelect
267281
options={
268-
field.allowSelectAll && (options.length > 0 || hasMore)
282+
field.allowSelectAll && (options.length > 0 || hasMore || allSelected)
269283
? [
270284
{
271-
value: '',
285+
value: field.selectAllValue ?? '',
272286
label: 'All',
273287
disabled: !isEnabled || hasSearch || isFetching || isLoadingAll,
274288
onSelect: () => void selectAll(),

0 commit comments

Comments
 (0)