Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- migrate:up
-- Admin-managed default app profile per (org, connector_key).
-- Today getPrimaryAuthProfileForKind picks the most-recently-updated active
-- oauth_app profile for the connector — admins have no way to designate
-- which profile members should fall through to. The flag lets the admin
-- pin a chosen profile; the resolver prefers flagged rows first.
--
-- Constrained to oauth_app for now since that's the only kind where
-- "default for connector" is meaningful (env / interactive / browser_session
-- are picked by other rules — device binding, capture mode, etc.).

ALTER TABLE auth_profiles
ADD COLUMN is_default_for_connector boolean NOT NULL DEFAULT false;

CREATE UNIQUE INDEX auth_profiles_default_for_connector_unique
ON auth_profiles (organization_id, connector_key)
WHERE is_default_for_connector AND profile_kind = 'oauth_app';

-- migrate:down
DROP INDEX IF EXISTS auth_profiles_default_for_connector_unique;

ALTER TABLE auth_profiles
DROP COLUMN IF EXISTS is_default_for_connector;
8 changes: 8 additions & 0 deletions db/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ CREATE TABLE public.auth_profiles (
browser_kind text,
user_data_dir text,
cdp_url text,
is_default_for_connector boolean DEFAULT false NOT NULL,
CONSTRAINT auth_profiles_browser_kind_check CHECK (((browser_kind IS NULL) OR (browser_kind = ANY (ARRAY['chrome'::text, 'brave'::text, 'arc'::text, 'edge'::text])))),
CONSTRAINT auth_profiles_connector_key_required CHECK (((connector_key IS NOT NULL) OR (profile_kind = 'browser_session'::text))),
CONSTRAINT auth_profiles_device_browser_path_mutex CHECK (((device_worker_id IS NULL) OR (profile_kind <> 'browser_session'::text) OR (user_data_dir IS NULL) OR (cdp_url IS NULL))),
Expand Down Expand Up @@ -2856,6 +2857,12 @@ CREATE INDEX agents_organization_id_idx ON public.agents USING btree (organizati

CREATE INDEX auth_profiles_connector_kind_idx ON public.auth_profiles USING btree (organization_id, connector_key, profile_kind, status);

--
-- Name: auth_profiles_default_for_connector_unique; Type: INDEX; Schema: public; Owner: -
--

CREATE UNIQUE INDEX auth_profiles_default_for_connector_unique ON public.auth_profiles USING btree (organization_id, connector_key) WHERE (is_default_for_connector AND (profile_kind = 'oauth_app'::text));

--
-- Name: auth_profiles_device_worker_idx; Type: INDEX; Schema: public; Owner: -
--
Expand Down Expand Up @@ -4999,6 +5006,7 @@ INSERT INTO public.schema_migrations (version) VALUES
('20260515120000'),
('20260515150000'),
('20260515160000'),
('20260515170000'),
('20260516120000'),
('20260516200000'),
('20260516200100');
31 changes: 30 additions & 1 deletion packages/server/src/auth/__tests__/tool-access.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,18 @@ describe('requiresOwnerAdmin', () => {
expect(
requiresOwnerAdmin('manage_connections', { action: 'update_connector_default_config' }, false)
).toBe(true);
});

it('should allow members to create + reauthenticate their own connections', () => {
// `manage_connections.create` and `reauthenticate` are member-write; the
// handler enforces app_auth_profile_slug must match the org default for
// non-admins, so non-admins can't bring an alternate OAuth client.
expect(requiresOwnerAdmin('manage_connections', { action: 'create' }, false)).toBe(false);
expect(requiresMemberWrite('manage_connections', { action: 'create' }, false)).toBe(true);
expect(requiresOwnerAdmin('manage_connections', { action: 'reauthenticate' }, false)).toBe(
false
);
expect(requiresMemberWrite('manage_connections', { action: 'reauthenticate' }, false)).toBe(
true
);
});
Expand All @@ -67,11 +78,29 @@ describe('requiresOwnerAdmin', () => {
expect(
requiresOwnerAdmin('manage_auth_profiles', { action: 'test_auth_profile' }, false)
).toBe(true);
expect(
requiresOwnerAdmin('manage_auth_profiles', { action: 'delete_auth_profile' }, false)
).toBe(true);
expect(
requiresOwnerAdmin('manage_auth_profiles', { action: 'set_default_auth_profile' }, false)
).toBe(true);
});

it('should allow members to create their own oauth_account profile', () => {
// create_auth_profile / update_auth_profile are member-write at the
// policy layer; the handler gates by profile_kind so non-oauth_account
// kinds (env, oauth_app, browser_session) stay admin-only.
expect(
requiresOwnerAdmin('manage_auth_profiles', { action: 'create_auth_profile' }, false)
).toBe(false);
expect(
requiresMemberWrite('manage_auth_profiles', { action: 'create_auth_profile' }, false)
).toBe(true);
expect(
requiresOwnerAdmin('manage_auth_profiles', { action: 'delete_auth_profile' }, false)
requiresOwnerAdmin('manage_auth_profiles', { action: 'update_auth_profile' }, false)
).toBe(false);
expect(
requiresMemberWrite('manage_auth_profiles', { action: 'update_auth_profile' }, false)
).toBe(true);
});

Expand Down
25 changes: 21 additions & 4 deletions packages/server/src/auth/tool-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,32 @@ const MEMBER_WRITE_ACTIONS: Record<string, Set<string> | null> = {
// via SDK namespace wrappers from inside `run_sdk`, and `routeAction` consults
// these tables to fire the same per-action access decisions.
manage_entity: new Set(['create', 'update', 'link', 'unlink', 'update_link']),
// Members can install connections that bind to their own OAuth account
// grant. The handler gates `app_auth_profile_slug` overrides against the
// org default + caller role, so members can't pick a non-default app
// profile.
manage_connections: new Set(['create', 'reauthenticate']),
// Members create / reconnect their own oauth_account profile. The handler
// gates `profile_kind` against role so env / oauth_app / browser_session
// stay admin-only.
manage_auth_profiles: new Set([
'create_auth_profile',
'update_auth_profile',
'test_auth_profile',
'get_auth_profile',
]),
};

const OWNER_ADMIN_ACTIONS: Record<string, Set<string>> = {
manage_entity: new Set(['delete']),
manage_entity_schema: new Set(['create', 'update', 'delete', 'add_rule', 'remove_rule']),
manage_connections: new Set([
'create',
// `create` and `reauthenticate` are in MEMBER_WRITE_ACTIONS — members
// install their own connections (handler enforces app_auth_profile slug
// override + role gates).
'update',
'delete',
'connect',
'reauthenticate',
'test',
'install_connector',
'uninstall_connector',
Expand All @@ -44,11 +59,13 @@ const OWNER_ADMIN_ACTIONS: Record<string, Set<string>> = {
]),
manage_feeds: new Set(['create_feed', 'update_feed', 'delete_feed', 'trigger_feed']),
manage_auth_profiles: new Set([
// `create_auth_profile` and `update_auth_profile` are in
// MEMBER_WRITE_ACTIONS — the handler enforces oauth_account-only access
// for non-admins so members can't create org-shared credentials.
'get_auth_profile',
'test_auth_profile',
'create_auth_profile',
'update_auth_profile',
'delete_auth_profile',
'set_default_auth_profile',
]),
manage_operations: new Set(['execute', 'approve', 'reject']),
manage_watchers: new Set([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -512,6 +512,7 @@ export function serializeAuthProfile(authProfile: AuthProfileRow): Record<string
browser_kind: authProfile.browser_kind,
user_data_dir: authProfile.user_data_dir,
cdp_url: authProfile.cdp_url,
is_default_for_connector: authProfile.is_default_for_connector,
...(authProfile.profile_kind === 'oauth_account'
? {
requested_scopes: readRequestedScopesFromAuthData(authProfile.auth_data),
Expand Down
Loading
Loading