Skip to content

Commit d1a9747

Browse files
feat(web): Add ability to refresh permissions from account linking settings (#945)
* feat(backend): add API endpoint to trigger account-driven permission sync (SOU-578) Adds POST /api/trigger-account-permission-sync that creates and enqueues an AccountPermissionSyncJob for a given accountId, with entitlement and provider validation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(web): generalize linked accounts with Linear-style UI - Introduces `LinkedAccount` type in `ee/features/sso/actions.ts` covering all OAuth providers (SSO + account_linking), replacing the narrower `LinkedAccountProviderState` - Rewrites linked accounts UI with Linear-style Connect / Connected dropdown pattern; dropdown includes Disconnect and Refresh Permissions actions - Adds `triggerAccountPermissionSync` server action and worker API call for per-account permission refresh - Renames settings page from "Permission Syncing" to "Linked Accounts" - Removes `getLinkedAccountProviderStates` in favour of `getLinkedAccounts` - Moves SSO-related components and actions from `ee/features/permissionSyncing/` to `ee/features/sso/` Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update CHANGELOG for #945 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * changelog * deprecate AUTH_EE_ALLOW_EMAIL_ACCOUNT_LINKING option and always enable it * add spinner when permissions are being refreshed * feedback * Add back AUTH_EE_ALLOW_EMAIL_ACCOUNT_LINKING. Change default to true * docs --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 355d7c0 commit d1a9747

File tree

34 files changed

+545
-463
lines changed

34 files changed

+545
-463
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Added login wall when anonymous users try to send messages on duplicated chats (askgh experiment). [#939](https://github.com/sourcebot-dev/sourcebot/pull/939)
1818
- Added `GET /api/ee/user` endpoint that returns the authenticated owner's user info (name, email, createdAt, updatedAt). [#940](https://github.com/sourcebot-dev/sourcebot/pull/940)
1919
- Added `selectedReposCount` to the `wa_chat_message_sent` PostHog event to track the number of selected repositories when users ask questions. [#941](https://github.com/sourcebot-dev/sourcebot/pull/941)
20+
- Added ability to re-sync repo permissions from the "linked accounts" settings page. [#945](https://github.com/sourcebot-dev/sourcebot/pull/945)
2021

2122
### Changed
2223
- Hide version upgrade toast for askgithub deployment (`EXPERIMENT_ASK_GH_ENABLED`). [#931](https://github.com/sourcebot-dev/sourcebot/pull/931)

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,16 @@ export const GET = apiHandler(async (request: NextRequest) => {
182182
});
183183
```
184184

185+
## Docs Images
186+
187+
Images added to `.mdx` files in `docs/` should be wrapped in a `<Frame>` component:
188+
189+
```mdx
190+
<Frame>
191+
<img src="/images/my_image.png" alt="Description" />
192+
</Frame>
193+
```
194+
185195
## Branches and Pull Requests
186196

187197
When creating a branch or opening a PR, ask the user for:

docs/docs/configuration/environment-variables.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ The following environment variables allow you to configure your Sourcebot deploy
4545
| `AUTH_EE_GCP_IAP_ENABLED` | `false` | <p>When enabled, allows Sourcebot to automatically register/login from a successful GCP IAP redirect</p> |
4646
| `AUTH_EE_GCP_IAP_AUDIENCE` | - | <p>The GCP IAP audience to use when verifying JWT tokens. Must be set to enable GCP IAP JIT provisioning</p> |
4747
| `EXPERIMENT_EE_PERMISSION_SYNC_ENABLED` | `false` | <p>Enables [permission syncing](/docs/features/permission-syncing).</p> |
48-
| `AUTH_EE_ALLOW_EMAIL_ACCOUNT_LINKING` | `false` | <p>When enabled, different SSO accounts with the same email address will automatically be linked.</p> |
48+
| `AUTH_EE_ALLOW_EMAIL_ACCOUNT_LINKING` | `true` | <p>When enabled, different SSO accounts with the same email address will automatically be linked.</p> |
4949

5050

5151
### Review Agent Environment Variables

docs/docs/features/permission-syncing.mdx

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ We are actively working on supporting more code hosts. If you'd like to see a sp
4747

4848
# Getting started
4949

50-
## GitHub
50+
### GitHub
5151

5252
Prerequisites:
5353
- Configure a [GitHub connection](/docs/connections/github).
@@ -65,7 +65,7 @@ Permission syncing works with **GitHub.com**, **GitHub Enterprise Cloud**, and *
6565
- A GitHub [external identity provider](/docs/configuration/idp#github) must be configured to (1) correlate a Sourcebot user with a GitHub user, and (2) to list repositories that the user has access to for [User driven syncing](/docs/features/permission-syncing#how-it-works).
6666
- OAuth tokens must assume the `repo` scope in order to use the [List repositories for the authenticated user API](https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-the-authenticated-user) during [User driven syncing](/docs/features/permission-syncing#how-it-works). Sourcebot **will only** use this token for **reads**.
6767

68-
## GitLab
68+
### GitLab
6969

7070
Prerequisites:
7171
- Configure a [GitLab connection](/docs/connections/gitlab).
@@ -80,7 +80,7 @@ Permission syncing works with **GitLab Self-managed** and **GitLab Cloud**. User
8080
- OAuth tokens require the `read_api` scope in order to use the [List projects for the authenticated user API](https://docs.gitlab.com/ee/api/projects.html#list-all-projects) during [User driven syncing](/docs/features/permission-syncing#how-it-works).
8181
- [Internal GitLab projects](https://docs.gitlab.com/user/public_access/#internal-projects-and-groups) are **not** enforced by permission syncing and therefore are visible to all users. Only [private projects](https://docs.gitlab.com/user/public_access/#private-projects-and-groups) are enforced.
8282

83-
## Bitbucket Cloud
83+
### Bitbucket Cloud
8484

8585
Prerequisites:
8686
- Configure a [Bitbucket Cloud connection](/docs/connections/bitbucket-cloud).
@@ -104,7 +104,7 @@ If your workspace relies heavily on group or project-level permissions rather th
104104
- A Bitbucket Cloud [external identity provider](/docs/configuration/idp#bitbucket-cloud) must be configured to (1) correlate a Sourcebot user with a Bitbucket Cloud user, and (2) to list repositories that the user has access to for [User driven syncing](/docs/features/permission-syncing#how-it-works).
105105
- OAuth tokens require the `account` and `repository` scopes. The `repository` scope is required to list private repositories during [User driven syncing](/docs/features/permission-syncing#how-it-works).
106106

107-
## Bitbucket Data Center
107+
### Bitbucket Data Center
108108

109109
Prerequisites:
110110
- Configure a [Bitbucket Data Center connection](/docs/connections/bitbucket-data-center).
@@ -138,4 +138,18 @@ User driven and repo driven syncing occurs every 24 hours by default. These inte
138138
| Setting | Type | Default | Minimum |
139139
|-------------------------------------------------|---------|------------|---------|
140140
| `experiment_repoDrivenPermissionSyncIntervalMs` | number | 24 hours | 1 |
141-
| `experiment_userDrivenPermissionSyncIntervalMs` | number | 24 hours | 1 |
141+
| `experiment_userDrivenPermissionSyncIntervalMs` | number | 24 hours | 1 |
142+
143+
## Manually refreshing permissions
144+
145+
If a user's permissions have changed and they need access updated immediately (without waiting for the next scheduled sync), they can trigger a manual refresh from the **Linked Accounts** page:
146+
147+
1. Navigate to **Settings → Linked Accounts**.
148+
2. Click the **Connected** button next to the relevant code host account.
149+
3. Select **Refresh Permissions** from the dropdown.
150+
151+
<Frame>
152+
<img src="/images/linked_accounts_refresh_permissions.png" alt="Linked Accounts - Refresh Permissions" />
153+
</Frame>
154+
155+
The button will show a spinner while the sync is in progress and display a confirmation once it completes.
111 KB
Loading

packages/backend/src/api.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { PrismaClient, RepoIndexingJobType } from '@sourcebot/db';
2-
import { createLogger } from '@sourcebot/shared';
2+
import { createLogger, env, hasEntitlement, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from '@sourcebot/shared';
33
import express, { Request, Response } from 'express';
44
import 'express-async-errors';
55
import * as http from "http";
66
import z from 'zod';
77
import { ConnectionManager } from './connectionManager.js';
8+
import { AccountPermissionSyncer } from './ee/accountPermissionSyncer.js';
89
import { PromClient } from './promClient.js';
910
import { RepoIndexManager } from './repoIndexManager.js';
1011
import { createGitHubRepoRecord } from './repoCompileUtils.js';
@@ -22,6 +23,7 @@ export class Api {
2223
private prisma: PrismaClient,
2324
private connectionManager: ConnectionManager,
2425
private repoIndexManager: RepoIndexManager,
26+
private accountPermissionSyncer: AccountPermissionSyncer,
2527
) {
2628
const app = express();
2729
app.use(express.json());
@@ -36,6 +38,7 @@ export class Api {
3638

3739
app.post('/api/sync-connection', this.syncConnection.bind(this));
3840
app.post('/api/index-repo', this.indexRepo.bind(this));
41+
app.post('/api/trigger-account-permission-sync', this.triggerAccountPermissionSync.bind(this));
3942
app.post(`/api/experimental/add-github-repo`, this.experimental_addGithubRepo.bind(this));
4043

4144
this.server = app.listen(PORT, () => {
@@ -96,6 +99,41 @@ export class Api {
9699
res.status(200).json({ jobId });
97100
}
98101

102+
private async triggerAccountPermissionSync(req: Request, res: Response) {
103+
if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED !== 'true' || !hasEntitlement('permission-syncing')) {
104+
res.status(403).json({ error: 'Permission syncing is not enabled.' });
105+
return;
106+
}
107+
108+
const schema = z.object({
109+
accountId: z.string(),
110+
}).strict();
111+
112+
const parsed = schema.safeParse(req.body);
113+
if (!parsed.success) {
114+
res.status(400).json({ error: parsed.error.message });
115+
return;
116+
}
117+
118+
const { accountId } = parsed.data;
119+
const account = await this.prisma.account.findUnique({
120+
where: { id: accountId },
121+
});
122+
123+
if (!account) {
124+
res.status(404).json({ error: 'Account not found' });
125+
return;
126+
}
127+
128+
if (!PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS.includes(account.provider as typeof PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS[number])) {
129+
res.status(400).json({ error: `Provider '${account.provider}' does not support permission syncing.` });
130+
return;
131+
}
132+
133+
const jobId = await this.accountPermissionSyncer.schedulePermissionSyncForAccount(account);
134+
res.status(200).json({ jobId });
135+
}
136+
99137
private async experimental_addGithubRepo(req: Request, res: Response) {
100138
const schema = z.object({
101139
owner: z.string(),

packages/backend/src/constants.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,8 @@
1-
import { CodeHostType } from "@sourcebot/db";
2-
import { env, IdentityProviderType } from "@sourcebot/shared";
1+
import { env } from "@sourcebot/shared";
32
import path from "path";
43

54
export const SINGLE_TENANT_ORG_ID = 1;
65

7-
export const PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES: CodeHostType[] = [
8-
'github',
9-
'gitlab',
10-
'bitbucketCloud',
11-
'bitbucketServer',
12-
];
13-
14-
export const PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS: IdentityProviderType[] = [
15-
'github',
16-
'gitlab',
17-
'bitbucket-cloud',
18-
'bitbucket-server',
19-
];
20-
216
export const REPOS_CACHE_DIR = path.join(env.DATA_CACHE_DIR, 'repos');
227
export const INDEX_CACHE_DIR = path.join(env.DATA_CACHE_DIR, 'index');
238

packages/backend/src/ee/accountPermissionSyncer.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import * as Sentry from "@sentry/node";
22
import { PrismaClient, AccountPermissionSyncJobStatus, Account, PermissionSyncSource} from "@sourcebot/db";
3-
import { env, hasEntitlement, createLogger, loadConfig, decryptOAuthToken } from "@sourcebot/shared";
3+
import { env, hasEntitlement, createLogger, loadConfig, decryptOAuthToken, PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from "@sourcebot/shared";
44
import { Job, Queue, Worker } from "bullmq";
55
import { Redis } from "ioredis";
6-
import { PERMISSION_SYNC_SUPPORTED_IDENTITY_PROVIDERS } from "../constants.js";
76
import {
87
createOctokitFromToken,
98
getOAuthScopesForAuthenticatedUser as getGitHubOAuthScopesForAuthenticatedUser,
@@ -116,6 +115,22 @@ export class AccountPermissionSyncer {
116115
await this.queue.close();
117116
}
118117

118+
public async schedulePermissionSyncForAccount(account: Account) {
119+
const [job] = await this.db.accountPermissionSyncJob.createManyAndReturn({
120+
data: [{ accountId: account.id }],
121+
});
122+
123+
await this.queue.add('accountPermissionSyncJob', {
124+
jobId: job.id,
125+
}, {
126+
removeOnComplete: env.REDIS_REMOVE_ON_COMPLETE,
127+
removeOnFail: env.REDIS_REMOVE_ON_FAIL,
128+
priority: 1,
129+
});
130+
131+
return job.id;
132+
}
133+
119134
private async schedulePermissionSync(accounts: Account[]) {
120135
// @note: we don't perform this in a transaction because
121136
// we want to avoid the situation where a job is created and run

packages/backend/src/ee/repoPermissionSyncer.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import * as Sentry from "@sentry/node";
22
import { PermissionSyncSource, PrismaClient, Repo, RepoPermissionSyncJobStatus } from "@sourcebot/db";
3-
import { createLogger } from "@sourcebot/shared";
3+
import { createLogger, PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "@sourcebot/shared";
44
import { env, hasEntitlement } from "@sourcebot/shared";
55
import { Job, Queue, Worker } from 'bullmq';
66
import { Redis } from 'ioredis';
7-
import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "../constants.js";
87
import { createOctokitFromToken, getRepoCollaborators, GITHUB_CLOUD_HOSTNAME } from "../github.js";
98
import { createGitLabFromPersonalAccessToken, getProjectMembers } from "../gitlab.js";
109
import { createBitbucketCloudClient, createBitbucketServerClient, getExplicitUserPermissionsForCloudRepo, getUserPermissionsForServerRepo } from "../bitbucket.js";

packages/backend/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const api = new Api(
8282
prisma,
8383
connectionManager,
8484
repoIndexManager,
85+
accountPermissionSyncer,
8586
);
8687

8788
logger.info('Worker started.');

0 commit comments

Comments
 (0)