Commit Graph

192 Commits

Author SHA1 Message Date
Ludovic Vielle
eccef41767 Adopt File type for trust logos and MCP
Trust GraphQL and MCP still exposed presigned URL strings for
trust-center logos while console and connect already serve stable
File.downloadUrl paths. Phase 1 migrates the seven public logo
fields on trust GraphQL and the trust-center file references on MCP
to the shared File type; trust GraphQL NDA stays on fileUrl for a
follow-up.

Trust resolvers load public files through filemanager and map them
with types.NewFile. The trust app Relay queries and components now
read logo.downloadUrl. MCP specification, resolvers, and helpers
are updated in sync, including NDA on MCP where callers already
have file access.

filemanager is split into focused files and its URL surface is
narrowed to GenerateFileURL(file) for stable app URLs and
GeneratePresignedURL for S3 redirects. GetPublicFile remains the
DB entry point when only a file ID is known.

Add trust and MCP e2e coverage for public logo download URLs.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-06-11 16:03:45 +02:00
Ludovic Vielle
e06f3e0520 Migrate Connect org logos to File type
Replace Organization.logoUrl and horizontalLogoUrl with nested File
objects whose downloadUrl points at /api/files/v1/public/{id}, matching
the Console migration.

Org logos are FileVisibilityPublic and served without HTTP auth, so
Connect File.downloadUrl is built eagerly in NewFile with no field-level
authorize. Logo loading moves to iam.OrganizationService.LogoFile and
HorizontalLogoFile; the old URL generators are removed.

Sync IAM Relay components and n8n organization operations. Add an e2e
test for Connect multipart logo upload and ExecuteConnectWithFile.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-06-11 13:55:44 +02:00
Sacha Al Himdani
9ac71f948f Update contact email to hello@probo.com
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-06-09 16:45:23 +02:00
Bryan Frimin
383ea5a2d4 Retrict some query and mutation to only session
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-06-02 17:43:21 -07:00
Cursor Agent
93ec0d73da Fix last owner demotion regression
Use the Connect schema's profile connection in the e2e regression so the test can find the sole owner's membership. Adjust whitespace around the resolver error path to satisfy Go lint.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
2026-05-28 18:37:44 -07:00
Cursor Agent
e9bcdc85f4 Reject last owner demotion
Return a conflict when membership role updates would demote the final active owner in an organization.

Add an end-to-end regression that verifies the mutation fails and leaves the owner role intact.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
2026-05-28 18:37:44 -07:00
Cursor Agent
8b6e9e420a Allow ordering profiles by email address
GraphQL profile ordering rejected EMAIL_ADDRESS because ProfileOrderField
did not expose this enum value in connect and console schemas.

Add EMAIL_ADDRESS to MembershipProfileOrderField and its validation list
so order input coercion accepts the value consistently. Extend
MembershipProfile cursor key encoding to support email ordering and avoid
runtime panics during pagination.

Update the MCP profile order enum to keep API surface definitions aligned
with the same ordering capability.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-28 12:23:17 -07:00
Sacha Al Himdani
e6b40957ee Add invitingOrganizations field on viewer
Expose viewer.invitingOrganizations: [Organization!]! returning the
organizations that have a live pending invitation directed at the
current identity (accepted_at IS NULL AND expires_at > NOW()). The
list is rendered under a "Pending invitations" section on the
memberships page and in the organization selector dropdown, so a user
already signed in with an existing identity can see which
organizations have invited them without having to dig through their
inbox.

The new field is gated by iam:invitation:list against the viewer's
own identity, so it does not loosen authorization on Organization
elsewhere. E2E coverage validates the live-pending case, the
no-invitation and post-accept cases, and a multi-org scenario.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-05-28 13:42:29 +02:00
Bryan Frimin
b717e71d1a Fix bad state serialization
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-27 19:23:43 -07:00
Cursor Agent
6f4ac54a27 Expose bridge sync errors in SCIM API
Add a dedicated SCIMBridge syncError field to the Connect GraphQL API
and populate it from the core bridge model.

Update the connector cards to read bridge state and syncError directly
from the bridge object instead of inferring failures from event
history.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-27 18:10:22 -07:00
Cursor Agent
0f8c9e1217 Pass scope into ArchiveUser
Change ArchiveUser to receive an explicit coredata scope from callers\ninstead of rebuilding scope from organization ID inside the service.\n\nWire connect and MCP resolvers to pass the authorized scope through to\nArchiveUser.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-27 12:23:18 -07:00
Cursor Agent
affed3c781 Remove extra delete error mapping
Drop the additional RemoveUser dependency-error handling that was added\noutside this PR scope.\n\nKeep the separate remove and archive operations, but let delete failures\npropagate through existing wrapped errors without the custom helper and\nreferenced-records error type.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-27 12:23:17 -07:00
Cursor Agent
1e08a23ddc Split user remove and archive actions
Restore RemoveUser as a hard delete operation and surface dependency\nconflicts with a dedicated IAM error.\n\nAdd a new ArchiveUser flow that deactivates profiles while keeping the\nmember in the organization, then expose both actions across Connect, MCP,\nCLI, n8n, console UI, and e2e coverage.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-27 12:23:17 -07:00
Cursor Agent
23070b18b5 Sync user archive across all interfaces
Align user-removal UX and API surface semantics with the new archive\nbehavior for manually managed users.\n\nFrontend copy and actions now use archive wording, and list rows are\nrefetched after the mutation so archived users reappear as inactive.\n\nMCP removeUser now documents and returns archived_user_id, n8n labels\nand response mapping now use archive semantics, and the CLI gains a\nuser archive command backed by the same mutation.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
2026-05-27 12:23:16 -07:00
Cursor Agent
b50bbc8d6a Archive manual users on remove
Switch remove-user behavior for manually managed profiles from hard\ndelete to archival by deactivating the profile. This matches the\nrequested SCIM-like lifecycle while avoiding dependency errors for\nlinked records such as signatures and assets.\n\nThe remove flow now updates profile state to INACTIVE, updates\nmembership timestamps, and emits a user-updated webhook event instead of\ndelete events. E2E coverage now asserts that remove keeps the profile\nand marks it inactive.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>
2026-05-27 12:23:07 -07:00
Cursor Agent
ffa3db3cd2 Return conflict when user has linked records
Map remove-user foreign key failures to a dedicated IAM conflict error so\nconsole users no longer receive a generic internal server error when a\nprofile is still referenced by signed documents, tasks, assets, or\nother records.\n\nThe service now detects wrapped dependency errors across both profile\nand membership deletes, and resolvers explicitly map the new error type\nto conflict responses. Added unit tests cover dependency detection and\nthe user-facing error message.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
2026-05-27 12:23:05 -07:00
Bryan Frimin
a862faee39 Wire batch authorize into server APIs
Add authz.NewBatchAuthorizeFunc — the batch counterpart to the
existing AuthorizeFunc — together with WithBatchAttr,
WithBatchSkipAssumptionCheck, and WithBatchDryRun options. It maps the
new batch errors (mixed organization, empty batch, unsupported
resource type) to GraphQL Invalid responses, and reuses the existing
mappings for ErrAssumptionRequired / ErrInsufficientPermissions /
ErrResourceNotFound.

Plumb the new function into the Connect and Console resolvers and add
Resolver.AuthorizeBatch to the MCP resolver with equivalent error
mapping for tool callers.

Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-23 13:31:36 -07:00
Bryan Frimin
4d1a98ffdb Create scope in Authorize
Signed-off-by: Bryan Frimin <bryan@probo.com>
2026-05-23 13:31:34 -07:00
Sacha Al Himdani
9e61e5e1b7 Return clear error on user delete FK violation
Deleting a membership profile referenced by other tables (owner,
approver, assignee, etc.) surfaced as a generic Internal error.
Detect the Postgres FK violation (23503) in the coredata Delete,
return ErrResourceInUse, and map it to CONFLICT in the GraphQL
and MCP resolvers so the client sees an actionable error.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-05-20 15:45:32 +02:00
Émile Ré
7ac77b85e9 Fix wsl_v5 lint errors and exclude node_modules from linting 2026-05-20 15:05:08 +04:00
Émile Ré
f5703d390b Enforce Go style rules across codebase
Apply five style rules: convert iota string enums to typed
string constants, replace errors.As with errors.AsType,
merge three-group imports into two groups, fix multiline
parameter/argument formatting, and replace fmt.Sprintf URL
construction with net/url.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-20 11:46:39 +04:00
Émile Ré
9156d6a16a Add wsl linter and fix
Signed-off-by: Émile Ré <emile@probo.com>
2026-05-20 09:27:28 +04:00
Sacha Al Himdani
a67f2433b1 Address code review findings in relay fetch and iam
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-05-12 18:42:54 +02:00
Bryan Frimin
5e55c888c4 Add Microsoft 365 SCIM bridge and access review driver
Microsoft 365's native SCIM endpoint is unreliable, so mirror the
Google Workspace bridge over Microsoft Graph: a new MICROSOFT_365
OAuth2 connector, a SCIM bridge provider listing /v1.0/users with
$select pagination, and an access review driver that derives admin
status from /directoryRoles members. Refactor the bridge runner to
share OAuth2 plumbing across providers and surface the new bridge
type, scopes, UI card, and bootstrap env wiring.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-05-06 15:41:30 +02:00
Sacha Al Himdani
88242eed87 Restore MCP cross-origin protection after go-sdk v1.6.0 bump
The go-sdk v1.6.0 release no longer applies a default
CrossOriginProtection when the field is nil in StreamableHTTPOptions,
silently removing Origin header verification. Wrap the streamable
handler with http.NewCrossOriginProtection().Handler(...) (the
recommended replacement, since the SDK field is deprecated).

Also regenerate gqlgen resolvers to track v0.17.90.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-05-05 14:46:04 +02:00
Bryan Frimin
2418079785 Introspect OAuth2 refresh tokens
RFC 7662 lets clients introspect any OAuth2 token, but the endpoint
only resolved access tokens. Look up refresh tokens too, honor the
optional token_type_hint to drive lookup order with a fallback to the
other table, and report revoked or expired refresh tokens as inactive.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-29 17:59:09 +04:00
Bryan Frimin
62f05b3ff2 Invalidate sessions on password change and reset
Session validity was decoupled from credential rotation: changing
a password (logged-in flow) or completing a forgot-password reset
left every existing iam_sessions row valid until its idle TTL.
A user who saw their account compromised on another device had
no way to actually evict that device by rotating the password.

Inside the same DB transaction as the password update, expire the
identity's other active sessions:

- ChangePassword keeps the caller's current session and revokes
  every other session for the identity, so the user is not
  logged out of the browser they just used.
- ResetPassword has no caller session (the user is anonymous,
  authenticated only by a stateless token) and revokes all of
  the identity's active sessions.

The session middleware already rejects rows with expire_reason
set, so revoked sessions are kicked out on the next request
without any middleware change.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-29 17:57:19 +04:00
Bryan Frimin
34d8bf10a2 Use forwarded headers for SCIM event client IP
The SCIM event handler read the client address from
r.RemoteAddr only, so when probod runs behind an HTTP
load balancer every event row recorded the load
balancer's internal IP. Switch to clientip.Extract,
which honors the Forwarded / X-Forwarded-For headers
already validated by trustedproxy middleware.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-29 17:56:26 +04:00
Émile Ré
272f63828f Add state to MCP profile and refactor contract filter
Add the profile state attribute (ACTIVE/INACTIVE) to the MCP
Profile schema so listUsers and getUser tools expose it, and
add a state filter to listUsers.

Rename excludeContractEnded to contractEnded across the entire
stack (MCP, GraphQL, CLI, frontend). The new boolean is two-way:
true returns only users with ended contracts, false returns only
users with active or no contract, and null returns all.

Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-27 16:06:44 +04:00
Bryan Frimin
0e701ec6e9 Style
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-19 12:00:53 +02:00
Bryan Frimin
11770b4058 Add OAuth2/OpenID Connect authorization server
Implement a full OAuth2 2.0 and OpenID Connect 1.0 authorization
server with support for authorization code flow (with PKCE),
refresh token rotation, device authorization grant, dynamic
client registration, token introspection, and token revocation.

Includes database schema, coredata layer, service logic, HTTP
handlers, OIDC discovery endpoint, and JWKS publishing.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-19 12:00:53 +02:00
Émile Ré
0ce1d8039a Split connect profile schema into profile, membership, and invitation
Move OIDCProviderInfo type into base.graphql alongside its query field
in both connect and trust APIs, removing orphan oidc.graphql files.
Split connect profile.graphql into three domain files: profile (with
user mutations), membership, and invitation.

Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-15 10:03:50 +04:00
Émile Ré
7fb19a63d9 Rename resolver files from dot to underscore separation
Change gqlgen filename_template from {name}.resolvers.go to
{name}_resolvers.go across all three APIs for consistent Go naming.

Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-15 09:19:40 +04:00
Émile Ré
925c53d689 Slim base.graphql to directives, scalars, Query, Node, and Mutation
Move Identity, Organization, Viewer, PageInfo, OrderDirection,
CountryCode, OIDCProviderInfo, File, and ReauthenticationReason out
of base.graphql into their own dedicated files across all three APIs.

base.graphql now only contains directives, scalars, Node interface,
Query type, and an empty Mutation type (required by Relay
schemaExtensions). Entity files use extend type Mutation for their
mutations.

Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-15 09:19:39 +04:00
Émile Ré
5f93071b20 Consolidate hub type definitions into their own files
Move Organization, Identity, TrustCenter, and Viewer definitions to
include all their connection fields directly, removing all extend type
blocks for these hub types from entity files.

This eliminates the Relay schemaExtensions constraint where extend type
could only target types defined in the main schema file. Entity files
now only define their own standalone types and extend type Mutation.

Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-15 09:19:39 +04:00
Émile Ré
31cca05ca4 Split GraphQL schemas into per-entity files
Split each API's monolithic schema.graphql into per-coredata-model
files under graphql/ subdirectories. gqlgen's follow-schema layout
with {name}.resolvers.go template generates one resolver file per
schema file. Relay uses schema + schemaExtensions to load the split
files.

Connect API: 8 files (base, session, organization, profile,
personal_api_key, saml, scim, audit_log)

Trust API: 5 files (base, trust_center, auth, nda, mailing_list)

Console API: 25 files covering all domain entities

Types extended across files (Organization, Mutation, Viewer,
TrustCenter, Identity) are defined in base.graphql as required by
Relay's schemaExtensions.

Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-15 09:19:38 +04:00
Sacha Al Himdani
d519361786 Allow authenticated users to query signUpEnabled
The signUpEnabled field used @session(required: NONE) which rejects
authenticated users. Change it to OPTIONAL so the query works regardless
of authentication state, fixing a crash on the sign-up page for
already-logged-in users.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-09 19:27:10 +02:00
Sacha Al Himdani
76f6c8f9d1 Show registration unavailable page when signup is disabled
Instead of showing the signup form and returning an internal error on
submit, the SignUpPage now queries signUpEnabled upfront and displays a
friendly message explaining that registration is not available, with a
link back to login.

Adds a signUpEnabled GraphQL query field on the connect/v1 API and
handles ErrSignupDisabled as a FORBIDDEN error in the SignUp resolver.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-09 18:28:59 +02:00
Aurélien Sibiril
22ccfaa30f Replace Organization.googleWorkspaceOAuth2Scopes with SCIMBridgeTypeInfo
A Google-Workspace-specific field on the generic Organization type
was future-hostile: each new SCIM bridge type would need its own
top-level field. Replace with a generic SCIMBridgeTypeInfo type
queried through Organization.scimBridgeTypes, parallel to the
ConnectorProviderInfo pattern in console/v1.

ConnectorList looks up the Google Workspace entry from the list
and passes its scopes to GoogleWorkspaceConnector as before.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-08 11:05:12 +02:00
Aurélien Sibiril
247cae14fa Expose OAuth2 scopes via GraphQL fields
Add per-context fields so the frontend can read scopes from the
type that owns each connection:

- ConnectorProviderInfo.oauth2Scopes: access review providers
- AccessSource.oauth2Scopes: access review reconnect flow
- Organization.slackOAuth2Scopes (console): compliance page Slack
- Organization.googleWorkspaceOAuth2Scopes (connect): SCIM bridge

Resolvers delegate to the constants declared in each owning Go
module. The Google Workspace field lives on Organization, not on
SCIMConfiguration, so the Connect button can read it before any
SCIM configuration exists.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-08 11:05:12 +02:00
Aurélien Sibiril
ff20b383d9 Add access review dependencies and enum tests
Add go-vcr dependency, dev config for new providers,
connector service changes for access review, connect
schema updates, and unit tests for enum Scan/Value.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
2026-04-02 14:49:13 +02:00
Émile Ré
f3982f23f9 Sanitize upon saving
Signed-off-by: Émile Ré <emile@getprobo.com>
2026-04-01 18:03:27 +04:00
Bryan Frimin
bb910b4314 Style
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-31 14:49:21 +02:00
Bryan Frimin
8095ac6233 Validate session transfer redirect with saferedirect
The session transfer handler was blindly redirecting to the continue URL
from the signed token. Use saferedirect with a trust center domain check
to prevent open redirects, and only trigger session transfer for known
trust center custom domains instead of any non-base-URL host.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-31 14:49:21 +02:00
Bryan Frimin
84a35c90e9 Add session transfer for SSO cookies on custom domains
After OIDC login, if the redirect targets a trust center custom
domain, the callback now redirects through a session-transfer
endpoint on that domain. The endpoint verifies an HMAC-signed,
time-limited token and sets the session cookie on the custom
domain before redirecting to the final URL.

The continue URL is bound into the signed token payload to
prevent open-redirect attacks via parameter tampering.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-31 14:49:20 +02:00
Sacha Al Himdani
851e585b9b Add ISC license headers to Go, TypeScript, and SQL files
Add ISC license headers to all .go, .ts, .tsx, and .sql files
using each file's git history to determine the correct copyright
year or year range. Trademarked icons (brand logos, vendor logos,
compliance framework logos) are excluded.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-03-31 13:31:30 +02:00
Bryan Frimin
419c93fc7d Fix compliance page login redirect to custom domains
SafeRedirect previously matched against a single static host string,
so OIDC callbacks always fell back to the console instead of
redirecting back to compliance pages on custom domains. Refactor
AllowedHost into a dynamic AllowedHostFunc and wire a trust-service
lookup into the connect handler so custom domain hosts are accepted.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-31 10:51:34 +02:00
Sacha Al Himdani
03d77c3bab Fix duplicate organization name returning internal error
When creating an organization with a name that produces a duplicate trust
center slug, the API now returns a Conflict response instead of a
generic Internal error.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-03-27 20:26:13 +01:00
Sacha Al Himdani
8c02c53315 Update copyright headers across all Go files
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-03-25 17:38:32 +01:00
Bryan Frimin
1d3cc1c65e Use single OIDC session auth method instead of per-provider values
The OIDC provider is already tracked in iam_oidc_states.provider, so
there is no need for provider-specific session auth methods (GOOGLE,
MICROSOFT). Replace them with a single OIDC auth method.

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-03-23 14:17:44 +01:00