-
Notifications
You must be signed in to change notification settings - Fork 146
Add access review campaigns #935
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
aureliensibiril
wants to merge
90
commits into
getprobo:main
Choose a base branch
from
aureliensibiril:aureliensibiril/eng-136-access-review
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+25,497
−179
Draft
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 27771c9
Add API key connector protocol
gearnode a89dc55
Add access review domain services
gearnode 1c0dcdc
Add access source driver interface
gearnode 1ede3e0
Add Google Workspace access source driver
gearnode e5cda07
Add Linear access source driver
gearnode 417a23e
Add Slack access source driver
gearnode 21f0972
Add Figma access source driver
gearnode 46ba2f0
Add 1Password access source driver
gearnode 674a454
Add HubSpot access source driver
gearnode 1672cf4
Add DocuSign access source driver
gearnode 7db02a7
Add Notion access source driver
gearnode b0268e5
Add Brex access source driver
gearnode 3fe4a5f
Add Tally access source driver
gearnode 9135362
Add Cloudflare access source driver
gearnode a8294f1
Add CSV access source driver
gearnode 1dd9d88
Add Probo memberships access source driver
gearnode 195314b
Add Sentry access source driver
gearnode e6e0775
Add OpenAI access source driver
gearnode 594b422
Add access review campaign service and worker
gearnode eb5a4d0
Add access review console GraphQL API
gearnode 2a08cc5
Add access review console UI
gearnode 117d7e9
Add access review MCP tools
gearnode 17c684a
Add e2e tests for access review API
gearnode 5bd9587
Add access review campaign detail page and clean up schema
gearnode ca16647
Refactor access review API and access source drivers
gearnode 088a35f
Replace goto LOOP with idiomatic for/select
aureliensibiril 644a873
Wrap bare errors in campaign service
aureliensibiril 40c1b34
Reduce campaign name max length to 255
aureliensibiril 1da51aa
Clean up name resolvers
aureliensibiril 5c4c17d
Add pagination guard to Cloudflare driver
aureliensibiril 270a362
Use ProfileStateActive const in memberships driver
aureliensibiril d0bb4f7
Fix import ordering in review engine
aureliensibiril 63139e1
Fix OrderField Column and validation
aureliensibiril 64237fb
Add Scoper to scope system and source fetch
aureliensibiril a720331
Add decision history to entity registry
aureliensibiril 058fdea
Use RETURNING in coredata Update methods
aureliensibiril 6513231
Clean up access review migrations
aureliensibiril b9c05d4
Add enum Scan/Value unit tests
aureliensibiril 0502f75
Rename CLI directory to kebab-case
aureliensibiril 49c94ab
Rename entry flag package to setflag
aureliensibiril 42a77df
Fix org-resolution error message wording
aureliensibiril c3c8073
Use Flags.Changed for csv-file in source update
aureliensibiril 461d486
Add confirmation prompt to start and close
aureliensibiril 5a528c0
Add exclusivity check for source create flags
aureliensibiril 25365c2
Add order-direction validation to list commands
aureliensibiril cfe8d4d
Revert "Use RETURNING in coredata Update methods"
aureliensibiril cd7c934
Improve Linear driver field coverage
aureliensibiril e106575
Add JobTitle and bot support to Slack driver
aureliensibiril f2cac15
Resolve HubSpot role names via roles endpoint
aureliensibiril b43b149
Include bots as ServiceAccount in Notion driver
aureliensibiril 4a37902
Fix Google Workspace role mapping and projection
aureliensibiril a54d49e
Concatenate all Cloudflare roles in driver
aureliensibiril e31fcf2
Add JobTitle to DocuSign driver
aureliensibiril 93a2fe7
Add Role, IsAdmin, CreatedAt to Probo driver
aureliensibiril f5db928
Add go-vcr test infrastructure for drivers
aureliensibiril 0e63996
Add go-vcr tests for existing drivers
aureliensibiril a27ef38
Add Sentry access review driver
aureliensibiril c8ff264
Add Supabase access review driver
aureliensibiril 822084f
Add GitHub access review driver
aureliensibiril 5326044
Add Intercom access review driver
aureliensibiril 5ae9803
Add Resend access review driver
aureliensibiril d66cb71
Wire new drivers into the review engine
aureliensibiril 15b9f56
Accept variable-length rows in CSV driver
aureliensibiril 0302ca8
Handle pagination parse errors in DocuSign driver
aureliensibiril b0b802b
Return error when pagination limit is reached
aureliensibiril d8a1288
Fix name worker retry on resolution failure
aureliensibiril d53150c
Add vendor icon components
aureliensibiril f9f6d89
Extract shared access review helpers
aureliensibiril 719b5be
Improve access review layout and source row
aureliensibiril c3dabf9
Refactor campaign pages to use shared helpers
aureliensibiril 21d189c
Fix access source dialog error handling
aureliensibiril 84174f7
Fix CSV access source page error handling
aureliensibiril c621d0e
Fix access source creation page
aureliensibiril 7afb890
Fix scope sources query returning all org sources
aureliensibiril 2186f0b
Add OAuth2 token exchange styles and client credentials grant
aureliensibiril ada13aa
Add bootstrap config for six new OAuth providers
aureliensibiril 219851f
Add ConnectorProviders helper and 1Password Users API settings
aureliensibiril 9581bd1
Add dynamic connector provider info and client credentials mutation
aureliensibiril 1e794ad
Add OAuth2 token refresh to review engine
aureliensibiril bf4b60a
Add Sentry org auto-discovery and 1Password Users API driver
aureliensibiril c5c6a43
Replace hardcoded providers with dynamic connector query
aureliensibiril 8db50fd
Add unit and E2E tests for connector operations
aureliensibiril 68c7329
Add VendorLogo component with tint support
aureliensibiril bb60c3c
Add search bar, vendor logos, and region select
aureliensibiril 62b2960
Fix VendorLogo tint to use Tailwind classes
aureliensibiril 24182c2
Add Linear and new OAuth providers to dev config
aureliensibiril be381b9
Add Brex and Linear OAuth connector config
aureliensibiril 61ca0ef
Add access source dialog with OAuth callback handling
aureliensibiril f22f338
Remove standalone CreateAccessSourcePage
aureliensibiril File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
86 changes: 86 additions & 0 deletions
86
apps/console/src/pages/organizations/access-reviews/AccessReviewLayout.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ); | ||
| } |
27 changes: 27 additions & 0 deletions
27
apps/console/src/pages/organizations/access-reviews/AccessReviewLayoutLoader.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ); | ||
| } | ||
170 changes: 170 additions & 0 deletions
170
apps/console/src/pages/organizations/access-reviews/CreateCsvAccessSourcePage.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ); | ||
| } |
28 changes: 28 additions & 0 deletions
28
apps/console/src/pages/organizations/access-reviews/CreateCsvAccessSourcePageLoader.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> | ||
| ); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.