Skip to content

Commit 811a39e

Browse files
authored
fix(integrations): show family service accounts on every product they authenticate (#6102)
* fix(integrations): show family service accounts on every product they authenticate An Atlassian API token authenticates Jira, Jira Service Management, and Confluence alike, so it is modeled as an `atlassian` pseudo-provider whose only service is named "Atlassian Service Account". Every credential display surface resolved through `getServiceConfigByProviderId`, which walks OAUTH_PROVIDERS in declaration order — so the credential resolved to that pseudo-service instead of to any product. The result: adding a service account from the Jira page, through a modal titled "Add Jira service account", produced a credential that appeared under neither Jira, JSM, nor Confluence, was titled "Atlassian Service Account" on its detail page, and lost its brand tile and category on the list. The same bug hid a Google service account everywhere except Gmail. - match credentials with `credentialProviderMatchesService`, which accepts a service's OAuth id or its service-account id - add `lib/integrations/credential-display.ts` as the single resolver for catalog join, mark, and copy, replacing three duplicated lookups that keyed the catalog by OAuth service *display name* — the reason the pseudo-service fell off the map - derive "family service account" from the catalog (a service-account id serving >1 integration) rather than hardcoding vendors, so a new integration joining a family needs no edit - title service-account detail pages by credential name, subtitle them with their reach, and state that reach up front on the connect form - keep the service description as the detail subtitle for every non-family credential, unchanged No schema, migration, contract, or persisted value changes; resolution is computed at render time from static config. Coverage for all 22 service-account provider ids is pinned in tests, including that the index and the predicate the Connected list filters on cannot drift apart. * chore(icons): use Atlassian's gradient marks for Jira and Confluence Replaces the flat #1868DB Jira and Confluence marks with Atlassian's gradient versions, matching the Atlassian mark added alongside them. - gradient ids go through `useId()` rather than the source SVGs' static ids, which would collide wherever two of these icons render on one page — the integrations list and the landing loops both do - pads the Atlassian viewBox so its artwork fills ~78% of the box, matching the inset Atlassian ships on the Jira and Confluence marks; without it the mark renders ~30% heavier than its siblings in the same tile Visual-only, but these marks render in ~60 files, so it is split from the credential fix to stay independently revertable. * fix(integrations): route the editor's service-account setup modal through the shared target The workflow editor's credential selector passed the OAuth service's own name and icon straight to ConnectServiceAccountModal, so opening the setup form from a Jira block titled it "Add Jira service account" while the integrations page and the chat — both of which already resolve through `useServiceAccountConnectTarget` — titled the same form "Add Atlassian service account". That is the exact confusion this branch set out to remove, surviving on the one surface that bypassed the shared resolver. * docs(atlassian): correct the service-account setup path and cover all three products The setup section could not be followed. It sent readers to a "Settings → Integrations tab" that does not exist (Integrations is a top-level workspace module) and told them to search the integrations list for "Atlassian Service Account", which matches no catalog entry — the catalog lists Jira, Jira Service Management, and Confluence. The page also described the credential as covering "Jira and Confluence" while listing Jira Service Management scopes, and the product now spells the coverage out in the connect form. - correct the path: Integrations -> Jira/JSM/Confluence -> Add to Sim -> Add service account - name all three products consistently, and state that one service account covers them - match the real button label ("Add service account")
1 parent aeb7eae commit 811a39e

15 files changed

Lines changed: 854 additions & 100 deletions

File tree

apps/docs/content/docs/en/integrations/atlassian-service-account.mdx

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
---
22
title: Atlassian Service Accounts
3-
description: Set up an Atlassian service account with a scoped API token to use Jira and Confluence in Sim workflows
3+
description: Set up an Atlassian service account with a scoped API token to use Jira, Jira Service Management, and Confluence in Sim workflows
44
---
55

66
import { Callout } from 'fumadocs-ui/components/callout'
77
import { Step, Steps } from 'fumadocs-ui/components/steps'
88
import { Image } from '@/components/ui/image'
99
import { FAQ } from '@/components/ui/faq'
1010

11-
Atlassian service accounts let your workflows authenticate to Jira and Confluence as a non-human bot user — independent of any individual employee's account. Each service account has its own email, its own permissions, and its own API tokens, all managed centrally in admin.atlassian.com.
11+
Atlassian service accounts let your workflows authenticate to Jira, Jira Service Management, and Confluence as a non-human bot user — independent of any individual employee's account. Each service account has its own email, its own permissions, and its own API tokens, all managed centrally in admin.atlassian.com.
1212

13-
This is the recommended way to use Jira and Confluence in production workflows: no one person's OAuth consent expires, the bot's permissions are auditable, and access can be revoked without touching anyone's personal account.
13+
This is the recommended way to use Atlassian products in production workflows: no one person's OAuth consent expires, the bot's permissions are auditable, and access can be revoked without touching anyone's personal account.
14+
15+
One service account covers all three products. You add it once, and it appears as a connected credential on the Jira, Jira Service Management, and Confluence integration pages alike — there is no separate credential to create per product.
1416

1517
## Prerequisites
1618

@@ -124,12 +126,17 @@ Your Atlassian site domain is the URL you use to access Jira or Confluence in yo
124126

125127
<Steps>
126128
<Step>
127-
Open your workspace **Settings** and go to the **Integrations** tab
129+
Open **Integrations** in your workspace sidebar
128130
</Step>
129131
<Step>
130-
Search for "Atlassian Service Account" and click it
132+
Open **Jira**, **Jira Service Management**, or **Confluence** — any of the three works, since they share one service account
131133

132-
{/* TODO(screenshot): Integrations page with "Atlassian Service Account" in the service list */}
134+
{/* TODO(screenshot): Integrations page with Jira in the list */}
135+
</Step>
136+
<Step>
137+
Click **Add to Sim** and choose **Add service account**
138+
139+
{/* TODO(screenshot): Jira integration page with the "Add to Sim" dropdown open */}
133140
</Step>
134141
<Step>
135142
Paste the API token, enter the site domain (e.g. `your-team.atlassian.net`), and optionally set a display name and description
@@ -145,15 +152,17 @@ Your Atlassian site domain is the URL you use to access Jira or Confluence in yo
145152
</div>
146153
</Step>
147154
<Step>
148-
Click **Add Service Account**. Sim verifies the token by calling Atlassian's `/myself` endpoint through the gateway — if it fails, you'll see a specific error explaining what went wrong.
155+
Click **Add service account**. Sim verifies the token by calling Atlassian's `/myself` endpoint through the gateway — if it fails, you'll see a specific error explaining what went wrong.
149156
</Step>
150157
</Steps>
151158

152159
The token, domain, and discovered cloudId are encrypted before being stored.
153160

161+
Once added, the credential is listed under **Connected** on all three Atlassian integration pages. It is named after the service account's own Atlassian display name, so several service accounts on the same site stay easy to tell apart.
162+
154163
## Using the Service Account in Workflows
155164

156-
Add a Jira or Confluence block to your workflow. In the credential dropdown, your Atlassian service account appears alongside any OAuth credentials. Select it and configure the block as you normally would.
165+
Add a Jira, Jira Service Management, or Confluence block to your workflow. In the credential dropdown, your Atlassian service account appears alongside any OAuth credentials. Select it and configure the block as you normally would.
157166

158167
<div className="flex justify-center">
159168
<Image
@@ -170,7 +179,7 @@ The block calls Atlassian's API gateway (`api.atlassian.com/ex/jira/{cloudId}/..
170179
<FAQ items={[
171180
{ question: "Why an API token instead of OAuth?", answer: "API tokens for service accounts don't have a 1-hour expiry and don't require any user to consent. They're issued by an org admin and are stable until you revoke them — which is what you want for an automated workflow." },
172181
{ question: "Can a regular user create a service account?", answer: "No. Service accounts are an Atlassian organization-level feature and only an organization admin can create them." },
173-
{ question: "Can the same service account work with both Jira and Confluence?", answer: "Yes — give the service account access to both products on your site, and include scopes for both when you create the API token. Then connect it once in Sim and use it from either Jira or Confluence blocks." },
182+
{ question: "Can the same service account work with Jira, Jira Service Management, and Confluence?", answer: "Yes — one service account covers all three. Give it access to each product you need on your site, include scopes for each when you create the API token, then connect it once in Sim. It appears as a connected credential on all three integration pages and can be selected from any of their blocks." },
174183
{ question: "What if my workflow needs different permissions than the token has?", answer: "Either widen the token's scopes (revoke it and create a new one with more scopes), or grant the service account higher project/space roles in Jira or Confluence. Scope failures look like 401/403 errors with descriptive messages." },
175184
{ question: "How do I rotate the API token?", answer: "Create a new token from the same service account in admin.atlassian.com, update the credential in Sim with the new token, and once it's working, revoke the old one." },
176185
{ question: "Does this work with Atlassian Data Center / on-prem?", answer: "No — this integration uses Atlassian Cloud's API gateway (`api.atlassian.com`). For Data Center, use the OAuth flow or set up a self-hosted bot user." },

apps/sim/app/workspace/[workspaceId]/integrations/[block]/integration-block-detail.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ import { useQueryState } from 'nuqs'
99
import {
1010
blockTypeToIconMap,
1111
type Integration,
12+
resolveCredentialDisplay,
1213
resolveOAuthServiceForIntegration,
1314
} from '@/lib/integrations'
14-
import { getServiceConfigByProviderId } from '@/lib/oauth'
15+
import { credentialProviderMatchesService } from '@/lib/oauth'
1516
import { ConnectOAuthModal } from '@/app/workspace/[workspaceId]/components/connect-oauth-modal'
1617
import { IntegrationSkillsSection } from '@/app/workspace/[workspaceId]/integrations/[block]/integration-skills-section'
1718
import { connectParam } from '@/app/workspace/[workspaceId]/integrations/[block]/search-params'
@@ -65,13 +66,21 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
6566

6667
useScrollRestoration(scrollContainerRef, { ready: !credentialsLoading })
6768

69+
/**
70+
* Credentials that authenticate this integration. Matching goes through
71+
* `credentialProviderMatchesService` so a family service account lists on
72+
* every product it powers — one Atlassian token covers Jira, Jira Service
73+
* Management, and Confluence. Comparing resolved `providerId`s instead would
74+
* hide it from all three, since `atlassian-service-account` resolves to its
75+
* own pseudo-service rather than to any product.
76+
*/
6877
const connectedCredentials = useMemo(() => {
6978
if (!oauthService) return []
7079
return credentials.filter(
7180
(c) =>
7281
(c.type === 'oauth' || c.type === 'service_account') &&
7382
c.providerId &&
74-
getServiceConfigByProviderId(c.providerId)?.providerId === oauthService.providerId
83+
credentialProviderMatchesService(c.providerId, oauthService)
7584
)
7685
}, [credentials, oauthService])
7786
const [serviceAccountOpen, setServiceAccountOpen] = useState(false)
@@ -112,7 +121,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
112121
{
113122
value: CONNECT_MODE.serviceAccount,
114123
label: serviceAccountConnectLabel,
115-
icon: oauthService.serviceIcon,
124+
icon: serviceAccountTarget?.serviceIcon ?? oauthService.serviceIcon,
116125
},
117126
]
118127
: []
@@ -170,14 +179,14 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
170179
serviceIcon={oauthService.serviceIcon}
171180
/>
172181
)}
173-
{hasServiceAccount && oauthService?.serviceAccountProviderId && (
182+
{hasServiceAccount && serviceAccountTarget && (
174183
<ConnectServiceAccountModal
175184
open={serviceAccountOpen}
176185
onOpenChange={setServiceAccountOpen}
177186
workspaceId={workspaceId}
178-
serviceAccountProviderId={oauthService.serviceAccountProviderId}
179-
serviceName={oauthService.serviceName}
180-
serviceIcon={oauthService.serviceIcon}
187+
serviceAccountProviderId={serviceAccountTarget.serviceAccountProviderId}
188+
serviceName={serviceAccountTarget.serviceName}
189+
serviceIcon={serviceAccountTarget.serviceIcon}
181190
/>
182191
)}
183192
<div
@@ -219,7 +228,7 @@ export function IntegrationBlockDetail({ integration, workspaceId }: Integration
219228
{credential.displayName}
220229
</span>
221230
<span className='truncate text-[12px] text-[var(--text-muted)]'>
222-
{credential.description || oauthService?.serviceName}
231+
{credential.description || resolveCredentialDisplay(credential).subtitle}
223232
</span>
224233
</div>
225234
<ArrowRight className='size-4 flex-shrink-0 text-[var(--text-icon)]' />

apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
getTokenServiceAccountDescriptor,
2323
type TokenServiceAccountProviderId,
2424
} from '@/lib/credentials/token-service-accounts/descriptors'
25+
import { getServiceAccountCoverageSentence } from '@/lib/integrations/credential-display'
2526
import {
2627
ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID,
2728
SLACK_CUSTOM_BOT_PROVIDER_ID,
@@ -60,6 +61,16 @@ function openDocs(url: string): void {
6061
*/
6162
const ATLASSIAN_DOMAIN_HINT_REGEX = /^[a-z0-9-]+\.atlassian\.net$/i
6263

64+
/**
65+
* States the site-wide reach of the token up front. Users reaching this modal
66+
* from the Jira page were left unsure whether they had connected Jira or Jira
67+
* Service Management; the credential covers both, plus Confluence. Derived from
68+
* the catalog so it cannot drift as Atlassian integrations are added.
69+
*/
70+
const ATLASSIAN_COVERAGE_HINT = getServiceAccountCoverageSentence(
71+
ATLASSIAN_SERVICE_ACCOUNT_PROVIDER_ID
72+
)
73+
6374
/**
6475
* Maps server `error.code` values returned by the Atlassian service-account
6576
* route to user-facing messages. Falls back to {@link FALLBACK_ERROR_MESSAGE}
@@ -524,6 +535,7 @@ function AtlassianServiceAccountModal({
524535
? 'Atlassian sites usually look like your-team.atlassian.net.'
525536
: undefined
526537
}
538+
hint={ATLASSIAN_COVERAGE_HINT}
527539
/>
528540

529541
<ChipModalField

apps/sim/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/use-service-account-connect.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ import {
55
getServiceAccountConnectNoun,
66
getServiceAccountGatingBlockType,
77
} from '@/lib/credentials/service-account-provider-ids'
8+
/**
9+
* Imported from the module rather than the `@/lib/integrations` barrel: the
10+
* barrel builds `POPULAR_WORKFLOWS` by calling `getAllBlockMeta()` at module
11+
* load, so importing it from a leaf component drags the whole block registry
12+
* into that component's graph.
13+
*/
14+
import {
15+
getServiceAccountFamilyIcon,
16+
getServiceAccountFamilyName,
17+
} from '@/lib/integrations/credential-display'
818
import { SLACK_CUSTOM_BOT_PROVIDER_ID } from '@/lib/oauth/types'
919
import type { ServiceAccountProviderId } from '@/app/workspace/[workspaceId]/integrations/components/connect-service-account-modal/connect-service-account-modal'
1020
import { getBlock } from '@/blocks'
@@ -17,6 +27,13 @@ import { isHiddenUnder, overlayVisibility } from '@/blocks/visibility/context'
1727
*/
1828
export interface ServiceAccountConnectTarget {
1929
serviceAccountProviderId: ServiceAccountProviderId
30+
/**
31+
* Name the setup surface is titled with. For a family service account this is
32+
* the vendor ("Atlassian"), not the product page you came from — one Atlassian
33+
* token authenticates Jira, Jira Service Management, and Confluence alike, so
34+
* calling it a "Jira service account" is what made users think they had
35+
* connected the wrong product.
36+
*/
2037
serviceName: string
2138
serviceIcon: ComponentType<{ className?: string }>
2239
/**
@@ -71,6 +88,15 @@ export function useServiceAccountConnectTarget({
7188
? 'Set up a custom bot'
7289
: `Add ${getServiceAccountConnectNoun(serviceAccountProviderId)}`
7390

74-
return { serviceAccountProviderId, serviceName, serviceIcon, label, hidden }
91+
const familyName = getServiceAccountFamilyName(serviceAccountProviderId)
92+
const familyIcon = getServiceAccountFamilyIcon(serviceAccountProviderId)
93+
94+
return {
95+
serviceAccountProviderId,
96+
serviceName: familyName ?? serviceName,
97+
serviceIcon: familyIcon ?? serviceIcon,
98+
label,
99+
hidden,
100+
}
75101
}, [serviceAccountProviderId, serviceName, serviceIcon, isSlackBot, hidden])
76102
}

apps/sim/app/workspace/[workspaceId]/integrations/connected/[credentialId]/connected-credential-detail.tsx

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import { createLogger } from '@sim/logger'
1616
import { getErrorMessage } from '@sim/utils/errors'
1717
import { useRouter } from 'next/navigation'
1818
import { writeOAuthReturnContext } from '@/lib/credentials/client-state'
19-
import { INTEGRATIONS, resolveOAuthServiceForIntegration } from '@/lib/integrations'
20-
import { getServiceConfigByProviderId } from '@/lib/oauth'
19+
import { resolveCredentialDisplay } from '@/lib/integrations'
2120
import {
2221
AddPeopleModal,
2322
CredentialDetailHeading,
@@ -97,27 +96,18 @@ export function ConnectedCredentialDetail({
9796
[oauthServiceNameByProviderId]
9897
)
9998

100-
const serviceConfig = useMemo(() => {
101-
if (!credential?.providerId) return null
102-
return getServiceConfigByProviderId(credential.providerId)
103-
}, [credential])
104-
10599
/**
106-
* Resolve the integration block type from the credential's OAuth service so
107-
* the header tile can render with the same brand background used by the rows
108-
* on the integrations list page. Several integrations can share one service
109-
* (e.g. Jira and Jira Service Management); the one named after the service
110-
* is preferred since it is the service's canonical integration.
100+
* Service, brand tile, and copy all come from the shared resolver so this
101+
* page, the integrations list, and the Cmd-K search agree on how a credential
102+
* is named and branded — a family service account reads as its family
103+
* ("Atlassian"), not as whichever product the provider walk happened to hit.
111104
*/
112-
const integrationBlockType = useMemo(() => {
113-
if (!serviceConfig) return ''
114-
const candidates = INTEGRATIONS.filter(
115-
(i) => resolveOAuthServiceForIntegration(i)?.providerId === serviceConfig.providerId
116-
)
117-
const serviceName = serviceConfig.name.toLowerCase()
118-
const canonical = candidates.find((i) => i.name.toLowerCase() === serviceName)
119-
return (canonical ?? candidates[0])?.type ?? ''
120-
}, [serviceConfig])
105+
const display = useMemo(
106+
() => (credential ? resolveCredentialDisplay(credential) : null),
107+
[credential]
108+
)
109+
const serviceConfig = display?.service ?? null
110+
const integrationBlockType = display?.blockType ?? ''
121111

122112
const handleReconnectOAuth = async () => {
123113
if (!credential || credential.type !== 'oauth' || !credential.providerId || !workspaceId) return
@@ -206,7 +196,7 @@ export function ConnectedCredentialDetail({
206196
: handleReconnectOAuth
207197
}
208198
disabled={connectOAuthService.isPending}
209-
leftIcon={serviceConfig?.icon}
199+
leftIcon={display?.icon ?? undefined}
210200
>
211201
Reconnect
212202
</Chip>
@@ -242,19 +232,16 @@ export function ConnectedCredentialDetail({
242232
)
243233
}
244234

245-
const serviceLabel =
246-
serviceConfig?.name || resolveProviderLabel(credential.providerId) || 'Unknown service'
235+
const headingTitle =
236+
display?.detailTitle || resolveProviderLabel(credential.providerId) || 'Unknown service'
247237

248238
return (
249239
<>
250240
<CredentialDetailLayout back={back} actions={actions}>
251241
<CredentialDetailHeading
252242
leading={
253-
serviceConfig ? (
254-
<IntegrationTile
255-
blockType={integrationBlockType}
256-
icon={serviceConfig.icon as ComponentType<{ className?: string }>}
257-
/>
243+
display?.icon ? (
244+
<IntegrationTile blockType={integrationBlockType} icon={display.icon} />
258245
) : (
259246
<div className='flex size-9 flex-shrink-0 items-center justify-center rounded-xl border border-[var(--border-1)] bg-[var(--bg)]'>
260247
<span className='font-medium text-[var(--text-tertiary)] text-small'>
@@ -263,8 +250,8 @@ export function ConnectedCredentialDetail({
263250
</div>
264251
)
265252
}
266-
title={serviceLabel}
267-
subtitle={serviceConfig?.description || 'Connected service'}
253+
title={headingTitle}
254+
subtitle={display?.detailSubtitle ?? 'Connected service'}
268255
/>
269256

270257
<DetailSection title='Credential ID'>
@@ -335,8 +322,8 @@ export function ConnectedCredentialDetail({
335322
onOpenChange={setReconnectOpen}
336323
workspaceId={workspaceId}
337324
serviceAccountProviderId={credential.providerId as ServiceAccountProviderId}
338-
serviceName={serviceConfig?.name || credential.displayName}
339-
serviceIcon={serviceConfig?.icon as ComponentType<{ className?: string }>}
325+
serviceName={display?.familyName || serviceConfig?.name || credential.displayName}
326+
serviceIcon={display?.icon as ComponentType<{ className?: string }>}
340327
credentialId={credential.id}
341328
credentialDisplayName={credential.displayName}
342329
credentialDescription={credential.description ?? undefined}

0 commit comments

Comments
 (0)