Skip to content

Commit bd5670c

Browse files
committed
fix(docs): group the app reference by client id, not by provider
One OAuth app is one client id, and more than one connector can register against it. `manageengine-sdp` reads `ZOHO_CLIENT_ID` (`lib/auth/connectors/providers.ts:1267`) while being its own provider entry, so grouping by provider left it off the Zoho section entirely: a self-hoster following that page registered one redirect URI, missed `/api/auth/oauth2/callback/manageengine-sdp`, and that connector's connect flow then failed redirect-URI validation. Same class as the Salesforce sandbox URI, from the same wrong assumption that a capability id maps to exactly one provider. Connectors sharing an app's client id are now read from the registrations themselves, which is where the runtime decides which credentials a connector uses, and attached to that app. Additive rather than a replacement: Instagram, Salesforce, Shopify and Trello run custom flows that declare no `clientId: env.X` line, so a pure registration scrape would have dropped four apps off the page. Also generalized the admin-consent warning, which named only `microsoft-ad` while Teams and Planner both request `Group.ReadWrite.All`. It now points at Microsoft's reference rather than implying every `.All` permission needs an admin, since `Sites.Read.All` does not.
1 parent 644f965 commit bd5670c

3 files changed

Lines changed: 77 additions & 6 deletions

File tree

apps/docs/content/docs/platform/self-hosting/integrations-oauth.mdx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ https://sim.yourdomain.com/api/auth/oauth2/callback/google-email
5757
https://sim.yourdomain.com/api/auth/oauth2/callback/google-drive
5858
```
5959

60-
Most providers let Sim request scopes at authorization time, but several need them declared on the app before anyone can connect. Microsoft Entra requires every delegated permission to be added to the registration, and the `microsoft-ad` connector's directory permissions additionally need tenant admin consent. Bitbucket fixes permissions on the consumer rather than per request. Google needs the matching API enabled on the Cloud project (Gmail API, Drive API, Calendar API, and so on) as well as the scopes on the consent screen.
60+
Most providers let Sim request scopes at authorization time, but several need them declared on the app before anyone can connect. Microsoft Entra requires every delegated permission to be added to the registration, and some of them cannot be granted by the connecting user at all: a tenant admin has to consent on the directory's behalf. The directory and group permissions are where this bites: `microsoft-ad` asks for several, and Teams and Planner both request `Group.ReadWrite.All`. Check each permission against [Microsoft's reference](https://learn.microsoft.com/en-us/graph/permissions-reference) rather than assuming a user can approve it, since not every `.All` permission needs an admin (`Sites.Read.All`, for one, does not). Bitbucket fixes permissions on the consumer rather than per request. Google needs the matching API enabled on the Cloud project (Gmail API, Drive API, Calendar API, and so on) as well as the scopes on the consent screen.
6161

6262
The [provider reference](#provider-reference) below lists the exact scopes each connector asks for, grouped by the app that covers it.
6363

@@ -386,9 +386,12 @@ Trello is API-key based rather than OAuth 2.0 and calls back to `/api/auth/trell
386386

387387
`ZOHO_CLIENT_ID` / `ZOHO_CLIENT_SECRET`
388388

389+
Every connector below also requests `aaaserver.profile.READ`.
390+
389391
| Connector | Provider ID | Scopes to grant |
390392
| --- | --- | --- |
391-
| Zoho Desk | `zoho-desk` | `Desk.tickets.READ`, `Desk.tickets.UPDATE`, `Desk.contacts.READ`, `Desk.articles.READ`, `Desk.organization.READ`, `Desk.agents.READ`, `Desk.basic.READ`, `Desk.webhooks.CREATE`, `Desk.webhooks.DELETE`, `aaaserver.profile.READ` |
393+
| Zoho Desk | `zoho-desk` | `Desk.tickets.READ`, `Desk.tickets.UPDATE`, `Desk.contacts.READ`, `Desk.articles.READ`, `Desk.organization.READ`, `Desk.agents.READ`, `Desk.basic.READ`, `Desk.webhooks.CREATE`, `Desk.webhooks.DELETE` |
394+
| ManageEngine ServiceDesk Plus | `manageengine-sdp` | `SDPOnDemand.requests.CREATE`, `SDPOnDemand.requests.READ`, `SDPOnDemand.requests.UPDATE`, `SDPOnDemand.requests.DELETE`, `SDPOnDemand.problems.CREATE`, `SDPOnDemand.problems.READ`, `SDPOnDemand.problems.UPDATE`, `SDPOnDemand.problems.DELETE`, `SDPOnDemand.changes.CREATE`, `SDPOnDemand.changes.READ`, `SDPOnDemand.changes.UPDATE`, `SDPOnDemand.changes.DELETE`, `SDPOnDemand.assets.CREATE`, `SDPOnDemand.assets.READ`, `SDPOnDemand.assets.UPDATE`, `SDPOnDemand.assets.DELETE`, `SDPOnDemand.solutions.CREATE`, `SDPOnDemand.solutions.READ`, `SDPOnDemand.solutions.UPDATE`, `SDPOnDemand.solutions.DELETE` |
392395

393396
### Zoom
394397

scripts/generate-docs.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
extractToolInfo,
1111
extractUserSettableParamIds,
1212
getToolInfo,
13+
loadClientIdEnvByProviderId,
1314
loadOAuthConnectCatalog,
1415
parseConstProperties,
1516
parsePropertiesContent,
@@ -1081,6 +1082,20 @@ describe('the self-hosting OAuth app reference', () => {
10811082
* them and listing one would send the reader looking for a client id that
10821083
* does not exist.
10831084
*/
1085+
/**
1086+
* One OAuth app is one client id, and more than one connector can be
1087+
* registered against it: `manageengine-sdp` reads `ZOHO_CLIENT_ID` despite
1088+
* being its own provider entry. Grouping by provider alone left its redirect
1089+
* URI and scopes off the page for the app that covers it.
1090+
*/
1091+
it('groups a connector under the app whose client id it registers against', () => {
1092+
const clientIdEnv = loadClientIdEnvByProviderId()
1093+
1094+
expect(clientIdEnv.get('manageengine-sdp')).toBe('ZOHO_CLIENT_ID')
1095+
expect(clientIdEnv.get('zoho-desk')).toBe('ZOHO_CLIENT_ID')
1096+
expect(clientIdEnv.get('sharepoint')).toBe('MICROSOFT_CLIENT_ID')
1097+
})
1098+
10841099
it('leaves out services that have no OAuth flow', () => {
10851100
const { services } = loadOAuthConnectCatalog()
10861101

scripts/generate-docs.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const LANDING_INTEGRATIONS_DATA_PATH = path.join(
5252
)
5353
const TRIGGERS_PATH = path.join(rootDir, 'apps/sim/triggers')
5454
const OAUTH_CONFIG_PATH = path.join(rootDir, 'apps/sim/lib/oauth/oauth.ts')
55+
const OAUTH_REGISTRATION_PATH = path.join(rootDir, 'apps/sim/lib/auth/connectors/providers.ts')
5556
const SELF_HOSTING_OAUTH_DOC_PATH = path.join(
5657
rootDir,
5758
'apps/docs/content/docs/platform/self-hosting/integrations-oauth.mdx'
@@ -1536,6 +1537,42 @@ const NON_STANDARD_OAUTH_CALLBACKS: Readonly<Record<string, string>> = {
15361537
const SELF_HOSTING_GENERATED_START = '{/* GENERATED-START:oauth-apps */}'
15371538
const SELF_HOSTING_GENERATED_END = '{/* GENERATED-END:oauth-apps */}'
15381539

1540+
/**
1541+
* Which client-id environment variable each connector registration reads.
1542+
*
1543+
* One OAuth app is one client id, and more than one connector can be
1544+
* registered against the same one: `manageengine-sdp` authenticates with
1545+
* `ZOHO_CLIENT_ID` even though it is its own provider entry. Grouping the
1546+
* reference by provider alone would leave that connector's redirect URI and
1547+
* scopes off the page of the app that covers it, and its connect flow would
1548+
* fail redirect-URI validation for anyone who registered from the docs.
1549+
*
1550+
* Read from the registrations themselves, since that is where the runtime
1551+
* decides which credentials a connector uses.
1552+
*/
1553+
export function loadClientIdEnvByProviderId(): Map<string, string> {
1554+
const byProviderId = new Map<string, string>()
1555+
const lines = readSourceFile(OAUTH_REGISTRATION_PATH).split('\n')
1556+
1557+
let providerId: string | null = null
1558+
for (const line of lines) {
1559+
const providerMatch = /^ {6}providerId: '([^']+)',$/.exec(line)
1560+
if (providerMatch) {
1561+
providerId = providerMatch[1]
1562+
continue
1563+
}
1564+
if (!providerId) continue
1565+
1566+
const clientIdMatch = /^ {6}clientId: env\.([A-Z0-9_]+) as string,$/.exec(line)
1567+
if (clientIdMatch) {
1568+
byProviderId.set(providerId, clientIdMatch[1])
1569+
providerId = null
1570+
}
1571+
}
1572+
1573+
return byProviderId
1574+
}
1575+
15391576
/**
15401577
* Build the per-OAuth-app reference for the self-hosting page.
15411578
*
@@ -1604,8 +1641,9 @@ export function sharedScopesAcrossConnectors(scopeLists: readonly (readonly stri
16041641
}
16051642

16061643
function buildSelfHostingOAuthReference(): string {
1607-
const { providers } = loadOAuthConnectCatalog()
1644+
const { providers, services } = loadOAuthConnectCatalog()
16081645

1646+
const clientIdEnvByProviderId = loadClientIdEnvByProviderId()
16091647
const apps = Object.keys(OAUTH_CLIENT_CAPABILITIES)
16101648
.map((capabilityId) => {
16111649
const provider = providers.get(capabilityId)
@@ -1615,18 +1653,33 @@ function buildSelfHostingOAuthReference(): string {
16151653
`lib/oauth/oauth.ts, so its app registration cannot be documented.`
16161654
)
16171655
}
1618-
return { capabilityId, provider }
1656+
1657+
// Connectors registered against this app's client id from some other
1658+
// provider entry. Additive rather than a replacement for the grouping
1659+
// above, because four capabilities (Instagram, Salesforce, Shopify,
1660+
// Trello) run custom flows that declare no `clientId: env.X` line.
1661+
const [clientIdEnv] =
1662+
OAUTH_CLIENT_CAPABILITIES[capabilityId as keyof typeof OAUTH_CLIENT_CAPABILITIES]
1663+
const own = new Set(provider.services.map((service) => service.serviceId))
1664+
const sharedConnectors = [...services.values()].filter(
1665+
(service) =>
1666+
!own.has(service.serviceId) &&
1667+
!service.serviceAccountOnly &&
1668+
clientIdEnvByProviderId.get(service.providerId) === clientIdEnv
1669+
)
1670+
1671+
return { capabilityId, provider, sharedConnectors }
16191672
})
16201673
.sort((a, b) => compareCatalogNames(a.provider.name, b.provider.name))
16211674

1622-
const sections = apps.map(({ capabilityId, provider }) => {
1675+
const sections = apps.map(({ capabilityId, provider, sharedConnectors }) => {
16231676
const envVars = OAUTH_CLIENT_CAPABILITIES[
16241677
capabilityId as keyof typeof OAUTH_CLIENT_CAPABILITIES
16251678
]
16261679
.map((name) => `\`${name}\``)
16271680
.join(' / ')
16281681

1629-
const connectors = provider.services
1682+
const connectors = [...provider.services, ...sharedConnectors]
16301683
.filter((service) => !service.serviceAccountOnly)
16311684
.map((service) => ({
16321685
service,

0 commit comments

Comments
 (0)