Skip to content
Open
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@
# Zendesk OAuth requires a Zendesk-approved global OAuth client (Marketplace).
# PROBOD_CONNECTOR_ZENDESK_CLIENT_ID=
# PROBOD_CONNECTOR_ZENDESK_CLIENT_SECRET=
# Google Analytics (GA4) OAuth (distinct from Google Workspace).
# PROBOD_CONNECTOR_GOOGLE_ANALYTICS_CLIENT_ID=
# PROBOD_CONNECTOR_GOOGLE_ANALYTICS_CLIENT_SECRET=
# PROBOD_CONNECTOR_SQUARE_CLIENT_ID=
# PROBOD_CONNECTOR_SQUARE_CLIENT_SECRET=
# PostHog Cloud (US + EU) OAuth needs NO config: it uses the CIMD public-client
# flow (no app registration, no client_secret), auto-enabled when this
# deployment is publicly reachable at PROBOD_BASE_URL. Self-hosted PostHog uses
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export function mapAPIKeyExtraSettingToField(
case "SCALEWAY":
if (settingKey === "organizationId") return "scalewayOrganizationId";
break;
case "SEGMENT":
if (settingKey === "region") return "segmentRegion";
break;
case "CRISP":
if (settingKey === "websiteId") return "crispWebsiteId";
break;
Expand Down
21 changes: 21 additions & 0 deletions packages/ui/src/Atoms/ThirdParties/Dotfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ComponentProps } from "react";

export function Dotfile(props: ComponentProps<"svg">) {
return (
<svg
viewBox="0 0 40.3633 40.3633"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
fill="#20804C"
d="M6.84835 20.2701C10.5023 16.6161 16.4266 16.6161 20.0806 20.2701C23.7345 23.924 23.7345 29.8483 20.0806 33.5023C16.4266 37.1562 10.5023 37.1562 6.84835 33.5023C3.19441 29.8483 3.19444 23.924 6.84835 20.2701Z"
/>
<path
fill="#101014"
d="M40.332 0.03125L40.3633 27.0244L27.0244 40.3633L26.9932 13.3701L0 13.3389L13.3389 0L40.332 0.03125Z"
/>
</svg>
);
}
12 changes: 12 additions & 0 deletions packages/ui/src/Atoms/ThirdParties/GoogleAnalytics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ComponentProps } from "react";

export function GoogleAnalytics(props: ComponentProps<"svg">) {
return (
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill="#E37400"
d="M22.84 2.9982v17.9987c.0086 1.6473-1.3197 2.9897-2.967 2.9984a2.9808 2.9808 0 01-.3677-.0208c-1.528-.226-2.6477-1.5558-2.6105-3.1V3.1204c-.0369-1.5458 1.0856-2.8762 2.6157-3.1 1.6361-.1915 3.1178.9796 3.3093 2.6158.014.1201.0208.241.0202.3619zM4.1326 18.0548c-1.6417 0-2.9726 1.331-2.9726 2.9726C1.16 22.6691 2.4909 24 4.1326 24s2.9726-1.3309 2.9726-2.9726-1.331-2.9726-2.9726-2.9726zm7.8728-9.0098c-.0171 0-.0342 0-.0513.0003-1.6495.0904-2.9293 1.474-2.891 3.1256v7.9846c0 2.167.9535 3.4825 2.3505 3.763 1.6118.3266 3.1832-.7152 3.5098-2.327.04-.1974.06-.3983.0593-.5998v-8.9585c.003-1.6474-1.33-2.9852-2.9773-2.9882z"
/>
</svg>
);
}
30 changes: 30 additions & 0 deletions packages/ui/src/Atoms/ThirdParties/Segment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ComponentProps } from "react";

export function Segment(props: ComponentProps<"svg">) {
return (
<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" {...props}>
<g transform="matrix(.768307 0 0 .768307 0 2.304922)">
<path
fill="#99cfac"
d="M51.9 52.8H4c-2.2 0-4-1.8-4-4s1.8-4 4-4h47.9c2.2 0 4 1.8 4 4s-1.8 4-4 4z"
/>
<path
fill="#49b881"
d="M41.7 77.3c-3.9 0-7.8-.6-11.5-1.7-2.1-.7-3.3-2.9-2.6-5s2.9-3.3 5-2.6c2.9.9 6 1.4 9.1 1.4 13.6 0 25.4-8.7 29.3-21.7.6-2.1 2.9-3.3 5-2.7s3.3 2.9 2.7 5c-5.1 16.3-19.9 27.3-37 27.3z"
/>
<path
fill="#99cfac"
d="M79.3 32.5H31.4c-2.2 0-4-1.8-4-4s1.8-4 4-4h47.9c2.2 0 4 1.8 4 4s-1.8 4-4 4z"
/>
<path
fill="#49b881"
d="M8.5 32.5c-.4 0-.8-.1-1.2-.2-2.1-.6-3.3-2.9-2.7-5C9.7 11 24.5 0 41.7 0c3.9 0 7.8.6 11.5 1.7 2.1.7 3.3 2.9 2.6 5s-2.9 3.3-5 2.6c-2.9-.9-6-1.4-9.1-1.4-13.6 0-25.4 8.7-29.3 21.7-.6 1.8-2.2 2.9-3.9 2.9z"
/>
<g fill="#99cfac">
<circle r="4" cy="13.3" cx="65.4" />
<circle r="4" cy="64.1" cx="17.9" />
</g>
</g>
</svg>
);
}
12 changes: 12 additions & 0 deletions packages/ui/src/Atoms/ThirdParties/Square.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ComponentProps } from "react";

export function Square(props: ComponentProps<"svg">) {
return (
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
fill="#3E4348"
d="M4.01 0A4.01 4.01 0 000 4.01v15.98c0 2.21 1.8 4 4.01 4.01h15.98C22.2 24 24 22.2 24 19.99V4A4.01 4.01 0 0019.99 0H4zm1.62 4.36h12.74c.7 0 1.26.57 1.26 1.27v12.74c0 .7-.56 1.27-1.26 1.27H5.63c-.7 0-1.26-.57-1.26-1.27V5.63a1.27 1.27 0 011.26-1.27zm3.83 4.35a.73.73 0 00-.73.73v5.09c0 .4.32.72.72.72h5.1a.73.73 0 00.73-.72V9.44a.73.73 0 00-.73-.73h-5.1Z"
/>
</svg>
);
}
8 changes: 8 additions & 0 deletions packages/ui/src/Atoms/ThirdParties/ThirdPartyLogo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ import { Cursor } from "./Cursor";
import { Datadog } from "./Datadog";
import { Deepgram } from "./Deepgram";
import { DocuSign } from "./DocuSign";
import { Dotfile } from "./Dotfile";
import { Figma } from "./Figma";
import { GitHub } from "./GitHub";
import { GitLab } from "./GitLab";
import { Google } from "./Google";
import { GoogleAnalytics } from "./GoogleAnalytics";
import { Grafana } from "./Grafana";
import { Heroku } from "./Heroku";
import { HubSpot } from "./HubSpot";
Expand All @@ -60,10 +62,12 @@ import { Railway } from "./Railway";
import { Render } from "./Render";
import { Resend } from "./Resend";
import { Scaleway } from "./Scaleway";
import { Segment } from "./Segment";
import { SendGrid } from "./SendGrid";
import { Sentry } from "./Sentry";
import { SigNoz } from "./SigNoz";
import { Slack } from "./Slack";
import { Square } from "./Square";
import { Supabase } from "./Supabase";
import { Tailscale } from "./Tailscale";
import { Tally } from "./Tally";
Expand All @@ -88,10 +92,12 @@ const thirdParties: Record<string, FC<ComponentProps<"svg">>> = {
DATADOG: Datadog,
DEEPGRAM: Deepgram,
DOCUSIGN: DocuSign,
DOTFILE: Dotfile,
FIGMA: Figma,
GITHUB: GitHub,
GITLAB: GitLab,
GOOGLE: Google,
GOOGLE_ANALYTICS: GoogleAnalytics,
GOOGLE_WORKSPACE: Google,
GRAFANA: Grafana,
HEROKU: Heroku,
Expand Down Expand Up @@ -121,10 +127,12 @@ const thirdParties: Record<string, FC<ComponentProps<"svg">>> = {
RENDER: Render,
RESEND: Resend,
SCALEWAY: Scaleway,
SEGMENT: Segment,
SENDGRID: SendGrid,
SENTRY: Sentry,
SIGNOZ: SigNoz,
SLACK: Slack,
SQUARE: Square,
SUPABASE: Supabase,
TAILSCALE: Tailscale,
TALLY: Tally,
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/Atoms/ThirdParties/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ export { Cursor } from "./Cursor";
export { Datadog } from "./Datadog";
export { Deepgram } from "./Deepgram";
export { DocuSign } from "./DocuSign";
export { Dotfile } from "./Dotfile";
export { Figma } from "./Figma";
export { GitHub } from "./GitHub";
export { GitLab } from "./GitLab";
export { Google } from "./Google";
export { GoogleAnalytics } from "./GoogleAnalytics";
export { Grafana } from "./Grafana";
export { Heroku } from "./Heroku";
export { HubSpot } from "./HubSpot";
Expand All @@ -44,10 +46,12 @@ export { Railway } from "./Railway";
export { Render } from "./Render";
export { Resend } from "./Resend";
export { Scaleway } from "./Scaleway";
export { Segment } from "./Segment";
export { SendGrid } from "./SendGrid";
export { Sentry } from "./Sentry";
export { SigNoz } from "./SigNoz";
export { Slack } from "./Slack";
export { Square } from "./Square";
export { Supabase } from "./Supabase";
export { Tally } from "./Tally";
export { Tailscale } from "./Tailscale";
Expand Down
193 changes: 193 additions & 0 deletions pkg/accessreview/drivers/dotfile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.

package drivers

import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

"go.probo.inc/probo/pkg/coredata"
)

const (
dotfileAPIHost = "api.dotfile.com"
dotfileUsersPath = "/v1/users"
dotfilePageSize = 100
)

// DotfileDriver lists the users of a single Dotfile workspace. The API key
// (sent in the X-DOTFILE-API-KEY header by the connection transport) is bound
// to one workspace, so GET /v1/users returns every user of that workspace with
// no tenant selector. Pagination is page/limit based.
type DotfileDriver struct {
httpClient *http.Client
}

var _ Driver = (*DotfileDriver)(nil)

type dotfileUser struct {
ID string `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Role string `json:"role"`
CreatedAt string `json:"created_at"`
// SuspendedAt is the suspension timestamp; null (decoded as empty) means
// the user is active. The endpoint returns only active users unless
// include_suspended=true is requested, so both states are enumerated.
SuspendedAt string `json:"suspended_at"`
}

type dotfileUsersResponse struct {
Data []dotfileUser `json:"data"`
Pagination struct {
Page int `json:"page"`
Limit int `json:"limit"`
Count int `json:"count"`
} `json:"pagination"`
}

func NewDotfileDriver(httpClient *http.Client) *DotfileDriver {
return &DotfileDriver{
httpClient: &http.Client{
Transport: &retryRoundTripper{
next: httpClient.Transport,
maxRetries: 3,
},
},
}
}

func (d *DotfileDriver) ListAccounts(ctx context.Context) ([]AccountRecord, error) {
var records []AccountRecord

collected := 0

for page := 1; page <= maxPaginationPages; page++ {
resp, err := d.fetchPage(ctx, page)
if err != nil {
return nil, err
}

for _, u := range resp.Data {
email := strings.TrimSpace(u.Email)
if email == "" {
continue
}

active := u.SuspendedAt == ""

records = append(records, AccountRecord{
Email: email,
FullName: dotfileFullName(u, email),
Roles: dotfileRoles(u.Role),
Active: &active,
IsAdmin: dotfileIsAdmin(u.Role),
MFAStatus: coredata.MFAStatusUnknown,
AuthMethod: coredata.AccessReviewEntryAuthMethodUnknown,
AccountType: coredata.AccessReviewEntryAccountTypeUser,
CreatedAt: parseRFC3339Ptr(u.CreatedAt),
ExternalID: u.ID,
})
}

collected += len(resp.Data)
if len(resp.Data) == 0 || collected >= resp.Pagination.Count {
return records, nil
}
}

return nil, fmt.Errorf("cannot list all dotfile accounts: %w", ErrPaginationLimitReached)
}

func (d *DotfileDriver) fetchPage(ctx context.Context, page int) (*dotfileUsersResponse, error) {
q := url.Values{}
q.Set("include_suspended", "true")
q.Set("limit", strconv.Itoa(dotfilePageSize))
q.Set("page", strconv.Itoa(page))

endpoint := url.URL{
Scheme: "https",
Host: dotfileAPIHost,
Path: dotfileUsersPath,
RawQuery: q.Encode(),
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
if err != nil {
return nil, fmt.Errorf("cannot create dotfile users request: %w", err)
}

req.Header.Set("Accept", "application/json")

httpResp, err := d.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("cannot execute dotfile users request: %w", err)
}

defer func() {
_ = httpResp.Body.Close()
}()

if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
return nil, fmt.Errorf("cannot fetch dotfile users: unexpected status %d", httpResp.StatusCode)
}

var resp dotfileUsersResponse
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
return nil, fmt.Errorf("cannot decode dotfile users response: %w", err)
}

return &resp, nil
}

// dotfileFullName joins the user's first and last names, falling back to the
// email when Dotfile exposes neither.
func dotfileFullName(u dotfileUser, email string) string {
name := strings.TrimSpace(strings.TrimSpace(u.FirstName) + " " + strings.TrimSpace(u.LastName))
if name == "" {
return email
}

return name
}

// dotfileRoles returns the user's single Dotfile role as a one-element slice
// (owner / admin / member / a custom role name), or an empty slice when none
// is set.
func dotfileRoles(role string) []string {
if r := strings.TrimSpace(role); r != "" {
return []string{r}
}

return []string{}
}

// dotfileIsAdmin reports whether the role grants administrative access. Dotfile
// has two system admin roles — owner (can delete the workspace) and admin (can
// change settings / invite) — while member and custom roles do not.
func dotfileIsAdmin(role string) bool {
switch strings.ToLower(strings.TrimSpace(role)) {
case "owner", "admin":
return true
default:
return false
}
}
Loading
Loading