Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
90 commits
Select commit Hold shift + click to select a range
dff46f4
Add access review data layer and migration
gearnode Mar 15, 2026
27771c9
Add API key connector protocol
gearnode Mar 15, 2026
a89dc55
Add access review domain services
gearnode Mar 15, 2026
1c0dcdc
Add access source driver interface
gearnode Mar 15, 2026
1ede3e0
Add Google Workspace access source driver
gearnode Mar 15, 2026
e5cda07
Add Linear access source driver
gearnode Mar 15, 2026
417a23e
Add Slack access source driver
gearnode Mar 15, 2026
21f0972
Add Figma access source driver
gearnode Mar 15, 2026
46ba2f0
Add 1Password access source driver
gearnode Mar 15, 2026
674a454
Add HubSpot access source driver
gearnode Mar 15, 2026
1672cf4
Add DocuSign access source driver
gearnode Mar 15, 2026
7db02a7
Add Notion access source driver
gearnode Mar 15, 2026
b0268e5
Add Brex access source driver
gearnode Mar 15, 2026
3fe4a5f
Add Tally access source driver
gearnode Mar 15, 2026
9135362
Add Cloudflare access source driver
gearnode Mar 15, 2026
a8294f1
Add CSV access source driver
gearnode Mar 15, 2026
1dd9d88
Add Probo memberships access source driver
gearnode Mar 15, 2026
195314b
Add Sentry access source driver
gearnode Mar 15, 2026
e6e0775
Add OpenAI access source driver
gearnode Mar 15, 2026
594b422
Add access review campaign service and worker
gearnode Mar 15, 2026
eb5a4d0
Add access review console GraphQL API
gearnode Mar 15, 2026
2a08cc5
Add access review console UI
gearnode Mar 15, 2026
117d7e9
Add access review MCP tools
gearnode Mar 15, 2026
17c684a
Add e2e tests for access review API
gearnode Mar 15, 2026
5bd9587
Add access review campaign detail page and clean up schema
gearnode Mar 24, 2026
ca16647
Refactor access review API and access source drivers
gearnode Mar 26, 2026
088a35f
Replace goto LOOP with idiomatic for/select
aureliensibiril Mar 26, 2026
644a873
Wrap bare errors in campaign service
aureliensibiril Mar 26, 2026
40c1b34
Reduce campaign name max length to 255
aureliensibiril Mar 26, 2026
1da51aa
Clean up name resolvers
aureliensibiril Mar 26, 2026
5c4c17d
Add pagination guard to Cloudflare driver
aureliensibiril Mar 26, 2026
270a362
Use ProfileStateActive const in memberships driver
aureliensibiril Mar 26, 2026
d0bb4f7
Fix import ordering in review engine
aureliensibiril Mar 26, 2026
63139e1
Fix OrderField Column and validation
aureliensibiril Mar 26, 2026
64237fb
Add Scoper to scope system and source fetch
aureliensibiril Mar 26, 2026
a720331
Add decision history to entity registry
aureliensibiril Mar 26, 2026
058fdea
Use RETURNING in coredata Update methods
aureliensibiril Mar 26, 2026
6513231
Clean up access review migrations
aureliensibiril Mar 26, 2026
b9c05d4
Add enum Scan/Value unit tests
aureliensibiril Mar 26, 2026
0502f75
Rename CLI directory to kebab-case
aureliensibiril Mar 26, 2026
49c94ab
Rename entry flag package to setflag
aureliensibiril Mar 26, 2026
42a77df
Fix org-resolution error message wording
aureliensibiril Mar 26, 2026
c3c8073
Use Flags.Changed for csv-file in source update
aureliensibiril Mar 26, 2026
461d486
Add confirmation prompt to start and close
aureliensibiril Mar 26, 2026
5a528c0
Add exclusivity check for source create flags
aureliensibiril Mar 26, 2026
25365c2
Add order-direction validation to list commands
aureliensibiril Mar 26, 2026
cfe8d4d
Revert "Use RETURNING in coredata Update methods"
aureliensibiril Mar 26, 2026
cd7c934
Improve Linear driver field coverage
aureliensibiril Mar 26, 2026
e106575
Add JobTitle and bot support to Slack driver
aureliensibiril Mar 26, 2026
f2cac15
Resolve HubSpot role names via roles endpoint
aureliensibiril Mar 26, 2026
b43b149
Include bots as ServiceAccount in Notion driver
aureliensibiril Mar 26, 2026
4a37902
Fix Google Workspace role mapping and projection
aureliensibiril Mar 26, 2026
a54d49e
Concatenate all Cloudflare roles in driver
aureliensibiril Mar 26, 2026
e31fcf2
Add JobTitle to DocuSign driver
aureliensibiril Mar 26, 2026
93a2fe7
Add Role, IsAdmin, CreatedAt to Probo driver
aureliensibiril Mar 26, 2026
f5db928
Add go-vcr test infrastructure for drivers
aureliensibiril Mar 26, 2026
0e63996
Add go-vcr tests for existing drivers
aureliensibiril Mar 26, 2026
a27ef38
Add Sentry access review driver
aureliensibiril Mar 26, 2026
c8ff264
Add Supabase access review driver
aureliensibiril Mar 26, 2026
822084f
Add GitHub access review driver
aureliensibiril Mar 26, 2026
5326044
Add Intercom access review driver
aureliensibiril Mar 26, 2026
5ae9803
Add Resend access review driver
aureliensibiril Mar 26, 2026
d66cb71
Wire new drivers into the review engine
aureliensibiril Mar 26, 2026
15b9f56
Accept variable-length rows in CSV driver
aureliensibiril Mar 26, 2026
0302ca8
Handle pagination parse errors in DocuSign driver
aureliensibiril Mar 26, 2026
b0b802b
Return error when pagination limit is reached
aureliensibiril Mar 26, 2026
d8a1288
Fix name worker retry on resolution failure
aureliensibiril Mar 26, 2026
d53150c
Add vendor icon components
aureliensibiril Mar 26, 2026
f9f6d89
Extract shared access review helpers
aureliensibiril Mar 26, 2026
719b5be
Improve access review layout and source row
aureliensibiril Mar 26, 2026
c3dabf9
Refactor campaign pages to use shared helpers
aureliensibiril Mar 26, 2026
21d189c
Fix access source dialog error handling
aureliensibiril Mar 26, 2026
84174f7
Fix CSV access source page error handling
aureliensibiril Mar 26, 2026
c621d0e
Fix access source creation page
aureliensibiril Mar 26, 2026
7afb890
Fix scope sources query returning all org sources
aureliensibiril Mar 26, 2026
2186f0b
Add OAuth2 token exchange styles and client credentials grant
aureliensibiril Mar 26, 2026
ada13aa
Add bootstrap config for six new OAuth providers
aureliensibiril Mar 26, 2026
219851f
Add ConnectorProviders helper and 1Password Users API settings
aureliensibiril Mar 26, 2026
9581bd1
Add dynamic connector provider info and client credentials mutation
aureliensibiril Mar 26, 2026
1e794ad
Add OAuth2 token refresh to review engine
aureliensibiril Mar 26, 2026
bf4b60a
Add Sentry org auto-discovery and 1Password Users API driver
aureliensibiril Mar 26, 2026
c5c6a43
Replace hardcoded providers with dynamic connector query
aureliensibiril Mar 26, 2026
8db50fd
Add unit and E2E tests for connector operations
aureliensibiril Mar 26, 2026
68c7329
Add VendorLogo component with tint support
aureliensibiril Mar 26, 2026
bb60c3c
Add search bar, vendor logos, and region select
aureliensibiril Mar 26, 2026
62b2960
Fix VendorLogo tint to use Tailwind classes
aureliensibiril Mar 26, 2026
24182c2
Add Linear and new OAuth providers to dev config
aureliensibiril Mar 26, 2026
be381b9
Add Brex and Linear OAuth connector config
aureliensibiril Mar 26, 2026
61ca0ef
Add access source dialog with OAuth callback handling
aureliensibiril Mar 26, 2026
f22f338
Remove standalone CreateAccessSourcePage
aureliensibiril Mar 26, 2026
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
11 changes: 11 additions & 0 deletions apps/console/src/pages/iam/organizations/_components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IconFire3,
IconGroup1,
IconInboxEmpty,
IconKey,
IconListStack,
IconLock,
IconMagnifyingGlass,
Expand Down Expand Up @@ -52,6 +53,9 @@ const fragment = graphql`
canListStatesOfApplicability: permission(
action: "core:state-of-applicability:list"
)
canListAccessReviewCampaigns: permission(
action: "core:access-review-campaign:list"
)
}
`;

Expand Down Expand Up @@ -186,6 +190,13 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) {
to={`${prefix}/snapshots`}
/>
)}
{organization.canListAccessReviewCampaigns && (
<SidebarItem
label={__("Access Reviews")}
icon={IconKey}
to={`${prefix}/access-reviews`}
/>
)}
{organization.canGetTrustCenter && (
<SidebarItem
label={__("Compliance Page")}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import {
IconFolder2,
IconKey,
PageHeader,
TabLink,
Tabs,
} from "@probo/ui";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { Outlet } from "react-router";
import { graphql } from "relay-runtime";

import type { AccessReviewLayoutQuery } from "#/__generated__/core/AccessReviewLayoutQuery.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";

export const accessReviewLayoutQuery = graphql`
query AccessReviewLayoutQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
__typename
... on Organization {
id
canCreateSource: permission(action: "core:access-source:create")
connectorProviderInfos {
provider
displayName
oauthConfigured
apiKeySupported
clientCredentialsSupported
extraSettings {
key
label
required
}
}
...AccessReviewCampaignsTabFragment
...AccessReviewSourcesTabFragment
}
}
}
`;

export default function AccessReviewLayout({
queryRef,
}: {
queryRef: PreloadedQuery<AccessReviewLayoutQuery>;
}) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();

usePageTitle(__("Access Reviews"));

const { organization } = usePreloadedQuery(accessReviewLayoutQuery, queryRef);
if (organization.__typename !== "Organization") {
throw new Error("Organization not found");
}

return (
<div className="space-y-6">
<PageHeader
title={__("Access Reviews")}
description={__(
"Review and manage user access across your organization's systems and applications.",
)}
/>

<Tabs>
<TabLink to={`/organizations/${organizationId}/access-reviews`} end>
<IconKey className="size-4" />
{__("Campaigns")}
</TabLink>
<TabLink to={`/organizations/${organizationId}/access-reviews/sources`}>
<IconFolder2 className="size-4" />
{__("Sources")}
</TabLink>
</Tabs>

<Outlet context={{
organizationRef: organization,
canCreateSource: organization.canCreateSource,
connectorProviderInfos: organization.connectorProviderInfos,
}}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Suspense, useEffect } from "react";
import { useQueryLoader } from "react-relay";

import type { AccessReviewLayoutQuery } from "#/__generated__/core/AccessReviewLayoutQuery.graphql";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import { useOrganizationId } from "#/hooks/useOrganizationId";

import AccessReviewLayout, { accessReviewLayoutQuery } from "./AccessReviewLayout";

export default function AccessReviewLayoutLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<AccessReviewLayoutQuery>(accessReviewLayoutQuery);

useEffect(() => {
if (!queryRef) {
loadQuery({ organizationId });
}
}, [loadQuery, organizationId]);

if (!queryRef) return <PageSkeleton />;

return (
<Suspense fallback={<PageSkeleton />}>
<AccessReviewLayout queryRef={queryRef} />
</Suspense>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { formatError, type GraphQLError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import {
Button,
Card,
Field,
PageHeader,
useToast,
} from "@probo/ui";
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
import { Link, useNavigate } from "react-router";
import { ConnectionHandler, graphql } from "relay-runtime";
import { z } from "zod";

import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql";
import type { CreateCsvAccessSourcePageQuery } from "#/__generated__/core/CreateCsvAccessSourcePageQuery.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useOrganizationId } from "#/hooks/useOrganizationId";

import { createAccessSourceMutation } from "./dialogs/CreateAccessSourceDialog";

export const createCsvAccessSourcePageQuery = graphql`
query CreateCsvAccessSourcePageQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
__typename
... on Organization {
id
canCreateSource: permission(action: "core:access-source:create")
}
}
}
`;

const csvSchema = z.object({
name: z.string().min(1),
csvData: z.string().min(1),
});

export default function CreateCsvAccessSourcePage({
queryRef,
}: {
queryRef: PreloadedQuery<CreateCsvAccessSourcePageQuery>;
}) {
const { __ } = useTranslate();
const { toast } = useToast();
const navigate = useNavigate();
const organizationId = useOrganizationId();
const { register, handleSubmit }
= useFormWithSchema(csvSchema, {
defaultValues: {
name: "",
csvData: "",
},
});

usePageTitle(__("Add CSV Access Source"));

const { organization } = usePreloadedQuery(createCsvAccessSourcePageQuery, queryRef);
if (organization.__typename !== "Organization") {
throw new Error("Organization not found");
}

const connectionId = ConnectionHandler.getConnectionID(
organization.id,
"AccessReviewSourcesTab_accessSources",
);

const [createAccessSource, isCreating]
= useMutation<CreateAccessSourceDialogMutation>(
createAccessSourceMutation,
);

if (!organization.canCreateSource) {
return (
<Card padded>
<p className="text-txt-secondary text-sm">
{__("You do not have permission to create access sources.")}
</p>
</Card>
);
}

const onSubmit = (data: z.infer<typeof csvSchema>) => {
createAccessSource({
variables: {
input: {
organizationId,
connectorId: null,
name: data.name,
csvData: data.csvData,
},
connections: connectionId ? [connectionId] : [],
},
onCompleted(_, errors) {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(
__("Failed to create access source"),
errors as GraphQLError[],
),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Access source created successfully."),
variant: "success",
});
void navigate(`/organizations/${organizationId}/access-reviews/sources`);
},
onError(error) {
toast({
title: __("Error"),
description: formatError(
__("Failed to create access source"),
error as GraphQLError,
),
variant: "error",
});
},
});
};

return (
<div className="space-y-6">
<PageHeader
title={__("Add CSV access source")}
description={__(
"Paste CSV content with a header row. This source will be saved and available in Access Reviews.",
)}
/>

<Card padded>
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
<Field
label={__("Name")}
{...register("name")}
type="text"
required
/>

<Field
label={__("CSV Data")}
{...register("csvData")}
type="textarea"
placeholder="email,full_name,role,job_title,is_admin,active,external_id"
required
/>
<p className="text-txt-secondary text-sm">
{__("Supported columns: email, full_name, role, job_title, is_admin, active, external_id.")}
</p>

<div className="flex items-center justify-end gap-2">
<Button variant="secondary" asChild>
<Link to={`/organizations/${organizationId}/access-reviews/sources`}>
{__("Back")}
</Link>
</Button>
<Button disabled={isCreating} type="submit">
{__("Create")}
</Button>
</div>
</form>
</Card>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Suspense, useEffect } from "react";
import { useQueryLoader } from "react-relay";

import type { CreateCsvAccessSourcePageQuery } from "#/__generated__/core/CreateCsvAccessSourcePageQuery.graphql";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import { useOrganizationId } from "#/hooks/useOrganizationId";

import CreateCsvAccessSourcePage, { createCsvAccessSourcePageQuery } from "./CreateCsvAccessSourcePage";

export default function CreateCsvAccessSourcePageLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery]
= useQueryLoader<CreateCsvAccessSourcePageQuery>(createCsvAccessSourcePageQuery);

useEffect(() => {
loadQuery({ organizationId });
}, [loadQuery, organizationId]);

if (!queryRef) {
return <PageSkeleton />;
}

return (
<Suspense fallback={<PageSkeleton />}>
<CreateCsvAccessSourcePage queryRef={queryRef} />
</Suspense>
);
}
Loading
Loading