- {pendingInvitations.map((invitation) => (
+ {invitations.map((invitation) => (
0 && (
- {pendingInvitations.length > 0 && (
+ {invitations.length > 0 && (
{__("Your organizations")}
@@ -166,18 +194,7 @@ export default function OrganizationsPage() {
}
type InvitationCardProps = {
- invitation: {
- id: string;
- email: string;
- fullName: string;
- role: string;
- expiresAt: string;
- createdAt: string;
- organization: {
- id: string;
- name: string;
- };
- };
+ invitation: Invitation;
onAccept: (invitationId: string, organizationId: string) => void;
isAccepting: boolean;
};
@@ -211,35 +228,106 @@ function InvitationCard({ invitation, onAccept, isAccepting }: InvitationCardPro
}
type OrganizationCardProps = {
- organization: {
- id: string;
- name: string;
- logoUrl: string | null | undefined;
- };
+ organization: Organization;
};
function OrganizationCard({ organization }: OrganizationCardProps) {
const { __ } = useTranslate();
+ const isAuthenticated = organization.authStatus === "authenticated";
+ const isExpired = organization.authStatus === "expired";
+ const needsAuth = organization.authStatus === "unauthenticated";
+
+ // Determine target URL and button text based on auth status
+ const targetUrl = isAuthenticated
+ ? `/organizations/${organization.id}`
+ : organization.loginUrl;
+
+ const getAuthBadge = () => {
+ if (isAuthenticated) {
+ return (
+
+
+ {__("Authenticated")}
+
+ );
+ }
+
+ if (isExpired) {
+ return (
+
+
+ {__("Session expired")}
+
+ );
+ }
+
+ if (needsAuth) {
+ return (
+
+
+ {__("Authentication required")}
+
+ );
+ }
+
+ return null;
+ };
+
+ const getButtonText = () => {
+ if (isAuthenticated) return __("Select");
+ if (organization.authenticationMethod === "saml") return __("Login with SAML");
+ return __("Login");
+ };
+
+ // Check if the URL is a backend SAML endpoint
+ const isSAMLUrl = targetUrl.includes('/auth/saml/');
+
return (
diff --git a/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts b/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts
deleted file mode 100644
index f73796a4d..000000000
--- a/apps/console/src/pages/__generated__/OrganizationsPageQuery.graphql.ts
+++ /dev/null
@@ -1,420 +0,0 @@
-/**
- * @generated SignedSource<<580387b50cff64395ba9afd4c7454d20>>
- * @lightSyntaxTransform
- * @nogrep
- */
-
-/* tslint:disable */
-/* eslint-disable */
-// @ts-nocheck
-
-import { ConcreteRequest } from 'relay-runtime';
-export type OrganizationsPageQuery$variables = Record;
-export type OrganizationsPageQuery$data = {
- readonly viewer: {
- readonly invitations: {
- readonly __id: string;
- readonly edges: ReadonlyArray<{
- readonly node: {
- readonly acceptedAt: any | null | undefined;
- readonly createdAt: any;
- readonly email: string;
- readonly expiresAt: any;
- readonly fullName: string;
- readonly id: string;
- readonly organization: {
- readonly id: string;
- readonly name: string;
- };
- readonly role: string;
- };
- }>;
- };
- readonly organizations: {
- readonly __id: string;
- readonly edges: ReadonlyArray<{
- readonly node: {
- readonly id: string;
- readonly logoUrl: string | null | undefined;
- readonly name: string;
- };
- }>;
- };
- };
-};
-export type OrganizationsPageQuery = {
- response: OrganizationsPageQuery$data;
- variables: OrganizationsPageQuery$variables;
-};
-
-const node: ConcreteRequest = (function(){
-var v0 = {
- "kind": "Literal",
- "name": "orderBy",
- "value": {
- "direction": "ASC",
- "field": "NAME"
- }
-},
-v1 = {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "id",
- "storageKey": null
-},
-v2 = {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "name",
- "storageKey": null
-},
-v3 = {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "__typename",
- "storageKey": null
-},
-v4 = {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "cursor",
- "storageKey": null
-},
-v5 = {
- "alias": null,
- "args": null,
- "concreteType": "PageInfo",
- "kind": "LinkedField",
- "name": "pageInfo",
- "plural": false,
- "selections": [
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "endCursor",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "hasNextPage",
- "storageKey": null
- }
- ],
- "storageKey": null
-},
-v6 = {
- "kind": "ClientExtension",
- "selections": [
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "__id",
- "storageKey": null
- }
- ]
-},
-v7 = [
- {
- "alias": null,
- "args": null,
- "concreteType": "OrganizationEdge",
- "kind": "LinkedField",
- "name": "edges",
- "plural": true,
- "selections": [
- {
- "alias": null,
- "args": null,
- "concreteType": "Organization",
- "kind": "LinkedField",
- "name": "node",
- "plural": false,
- "selections": [
- (v1/*: any*/),
- (v2/*: any*/),
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "logoUrl",
- "storageKey": null
- },
- (v3/*: any*/)
- ],
- "storageKey": null
- },
- (v4/*: any*/)
- ],
- "storageKey": null
- },
- (v5/*: any*/),
- (v6/*: any*/)
-],
-v8 = {
- "kind": "Literal",
- "name": "filter",
- "value": {
- "statuses": [
- "PENDING"
- ]
- }
-},
-v9 = {
- "kind": "Literal",
- "name": "orderBy",
- "value": {
- "direction": "DESC",
- "field": "CREATED_AT"
- }
-},
-v10 = [
- {
- "alias": null,
- "args": null,
- "concreteType": "InvitationEdge",
- "kind": "LinkedField",
- "name": "edges",
- "plural": true,
- "selections": [
- {
- "alias": null,
- "args": null,
- "concreteType": "Invitation",
- "kind": "LinkedField",
- "name": "node",
- "plural": false,
- "selections": [
- (v1/*: any*/),
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "email",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "fullName",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "role",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "expiresAt",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "acceptedAt",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "createdAt",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "concreteType": "Organization",
- "kind": "LinkedField",
- "name": "organization",
- "plural": false,
- "selections": [
- (v1/*: any*/),
- (v2/*: any*/)
- ],
- "storageKey": null
- },
- (v3/*: any*/)
- ],
- "storageKey": null
- },
- (v4/*: any*/)
- ],
- "storageKey": null
- },
- (v5/*: any*/),
- (v6/*: any*/)
-],
-v11 = {
- "kind": "Literal",
- "name": "first",
- "value": 1000
-},
-v12 = [
- (v11/*: any*/),
- (v0/*: any*/)
-],
-v13 = [
- (v8/*: any*/),
- (v11/*: any*/),
- (v9/*: any*/)
-];
-return {
- "fragment": {
- "argumentDefinitions": [],
- "kind": "Fragment",
- "metadata": null,
- "name": "OrganizationsPageQuery",
- "selections": [
- {
- "alias": null,
- "args": null,
- "concreteType": "Viewer",
- "kind": "LinkedField",
- "name": "viewer",
- "plural": false,
- "selections": [
- {
- "alias": "organizations",
- "args": [
- (v0/*: any*/)
- ],
- "concreteType": "OrganizationConnection",
- "kind": "LinkedField",
- "name": "__OrganizationsPage_organizations_connection",
- "plural": false,
- "selections": (v7/*: any*/),
- "storageKey": "__OrganizationsPage_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
- },
- {
- "alias": "invitations",
- "args": [
- (v8/*: any*/),
- (v9/*: any*/)
- ],
- "concreteType": "InvitationConnection",
- "kind": "LinkedField",
- "name": "__OrganizationsPage_invitations_connection",
- "plural": false,
- "selections": (v10/*: any*/),
- "storageKey": "__OrganizationsPage_invitations_connection(filter:{\"statuses\":[\"PENDING\"]},orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
- }
- ],
- "storageKey": null
- }
- ],
- "type": "Query",
- "abstractKey": null
- },
- "kind": "Request",
- "operation": {
- "argumentDefinitions": [],
- "kind": "Operation",
- "name": "OrganizationsPageQuery",
- "selections": [
- {
- "alias": null,
- "args": null,
- "concreteType": "Viewer",
- "kind": "LinkedField",
- "name": "viewer",
- "plural": false,
- "selections": [
- {
- "alias": null,
- "args": (v12/*: any*/),
- "concreteType": "OrganizationConnection",
- "kind": "LinkedField",
- "name": "organizations",
- "plural": false,
- "selections": (v7/*: any*/),
- "storageKey": "organizations(first:1000,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
- },
- {
- "alias": null,
- "args": (v12/*: any*/),
- "filters": [
- "orderBy"
- ],
- "handle": "connection",
- "key": "OrganizationsPage_organizations",
- "kind": "LinkedHandle",
- "name": "organizations"
- },
- {
- "alias": null,
- "args": (v13/*: any*/),
- "concreteType": "InvitationConnection",
- "kind": "LinkedField",
- "name": "invitations",
- "plural": false,
- "selections": (v10/*: any*/),
- "storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})"
- },
- {
- "alias": null,
- "args": (v13/*: any*/),
- "filters": [
- "orderBy",
- "filter"
- ],
- "handle": "connection",
- "key": "OrganizationsPage_invitations",
- "kind": "LinkedHandle",
- "name": "invitations"
- },
- (v1/*: any*/)
- ],
- "storageKey": null
- }
- ]
- },
- "params": {
- "cacheID": "1b2e528133e56da643b9f8214a2f7192",
- "id": null,
- "metadata": {
- "connection": [
- {
- "count": null,
- "cursor": null,
- "direction": "forward",
- "path": [
- "viewer",
- "organizations"
- ]
- },
- {
- "count": null,
- "cursor": null,
- "direction": "forward",
- "path": [
- "viewer",
- "invitations"
- ]
- }
- ]
- },
- "name": "OrganizationsPageQuery",
- "operationKind": "query",
- "text": "query OrganizationsPageQuery {\n viewer {\n organizations(first: 1000, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n invitations(first: 1000, orderBy: {field: CREATED_AT, direction: DESC}, filter: {statuses: [PENDING]}) {\n edges {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n organization {\n id\n name\n }\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n id\n }\n}\n"
- }
-};
-})();
-
-(node as any).hash = "ecd2534f36aa95ff7ff864ff22f09189";
-
-export default node;
diff --git a/apps/console/src/pages/auth/ConfirmEmailPage.tsx b/apps/console/src/pages/auth/ConfirmEmailPage.tsx
index f788e79a5..abaa29787 100644
--- a/apps/console/src/pages/auth/ConfirmEmailPage.tsx
+++ b/apps/console/src/pages/auth/ConfirmEmailPage.tsx
@@ -111,7 +111,7 @@ export default function ConfirmEmailPage() {
{__("Your email has been confirmed successfully!")}
-
@@ -141,7 +141,7 @@ export default function ConfirmEmailPage() {
{!isConfirmed && (
{__("Back to Login")}
diff --git a/apps/console/src/pages/auth/ForgotPasswordPage.tsx b/apps/console/src/pages/auth/ForgotPasswordPage.tsx
index 891bac129..085e6165e 100644
--- a/apps/console/src/pages/auth/ForgotPasswordPage.tsx
+++ b/apps/console/src/pages/auth/ForgotPasswordPage.tsx
@@ -23,7 +23,7 @@ export default function ForgotPasswordPage() {
const onSubmit = handleSubmit(async (data) => {
const response = await fetch(
- buildEndpoint("/api/console/v1/auth/forget-password"),
+ buildEndpoint("/auth/forget-password"),
{
method: "POST",
headers: {
@@ -81,7 +81,7 @@ export default function ForgotPasswordPage() {
{__("Remember your password?")}{" "}
{__("Back to login")}
@@ -124,7 +124,7 @@ export default function ForgotPasswordPage() {
{__("Remember your password?")}{" "}
{__("Back to login")}
diff --git a/apps/console/src/pages/auth/LoginPage.tsx b/apps/console/src/pages/auth/LoginPage.tsx
index e9c0965fc..f9d7e138c 100644
--- a/apps/console/src/pages/auth/LoginPage.tsx
+++ b/apps/console/src/pages/auth/LoginPage.tsx
@@ -1,81 +1,261 @@
import { useTranslate } from "@probo/i18n";
-import { Button, Field, useToast } from "@probo/ui";
+import { Button, Field, IconChevronLeft, useToast } from "@probo/ui";
import type { FormEventHandler } from "react";
-import { Link } from "react-router";
+import { useState } from "react";
+import { Link, useSearchParams } from "react-router";
import { buildEndpoint } from "/providers/RelayProviders";
export default function LoginPage() {
const { __ } = useTranslate();
const { toast } = useToast();
+ const [searchParams] = useSearchParams();
- const handleSubmit: FormEventHandler = async (e) => {
+ const authMethod = searchParams.get("method");
+ const initialMode = authMethod === "password" ? "password" : authMethod === "sso" ? "sso" : "default";
+
+ const [mode, setMode] = useState<"default" | "password" | "sso">(initialMode);
+ const [isLoading, setIsLoading] = useState(false);
+ const [isChecking, setIsChecking] = useState(false);
+
+ const handlePasswordLogin: FormEventHandler = async (e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
- const email = formData.get("email")?.toString();
- const password = formData.get("password")?.toString();
+ const emailValue = formData.get("email")?.toString();
+ const passwordValue = formData.get("password")?.toString();
- fetch(buildEndpoint("/api/console/v1/auth/login"), {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ email, password }),
- })
- .then(async (res) => {
- if (!res.ok) {
- const error = await res.json();
- throw new Error(error.message || __("Failed to login"));
- }
- window.location.href = "/";
- })
- .catch((e) => {
- toast({
- title: __("Error"),
- description: e.message as string,
- variant: "error",
- });
+ if (!emailValue || !passwordValue) return;
+
+ setIsLoading(true);
+
+ try {
+ const res = await fetch(buildEndpoint("/auth/login"), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email: emailValue, password: passwordValue }),
});
+
+ if (!res.ok) {
+ const error = await res.json();
+ throw new Error(error.message || __("Failed to login"));
+ }
+
+ window.location.href = "/";
+ } catch (e: any) {
+ toast({
+ title: __("Error"),
+ description: e.message as string,
+ variant: "error",
+ });
+ } finally {
+ setIsLoading(false);
+ }
};
+ const handleSSOLogin: FormEventHandler = async (e) => {
+ e.preventDefault();
+ const formData = new FormData(e.currentTarget);
+ const emailValue = formData.get("email")?.toString();
+
+ if (!emailValue) return;
+
+ setIsChecking(true);
+
+ try {
+ const res = await fetch(buildEndpoint("/auth/check-sso"), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ email: emailValue }),
+ });
+
+ if (!res.ok) {
+ const error = await res.json();
+ throw new Error(error.message || __("SSO not available for this email domain"));
+ }
+
+ const data = await res.json();
+
+ if (data.ssoAvailable && data.samlConfigId) {
+ window.location.href = buildEndpoint(
+ `/auth/saml/login/${data.samlConfigId}`
+ );
+ } else {
+ throw new Error(__("SSO not available for this email domain"));
+ }
+ } catch (e: any) {
+ toast({
+ title: __("Error"),
+ description: e.message as string,
+ variant: "error",
+ });
+ } finally {
+ setIsChecking(false);
+ }
+ };
+
+ const handleBack = () => {
+ setMode("default");
+ };
+
+ if (mode === "default") {
+ return (
+
+
+ {__("Login to your account")}
+
+
+ {__("Choose your login method")}
+
+
+
setMode("password")}
+ >
+ {__("Login with Email")}
+
+
+
+
+
setMode("sso")}
+ >
+ {__("Login with SSO")}
+
+
+
+ {__("Don't have an account ?")}{" "}
+
+ {__("Register")}
+
+
+
+
+ {__("Forgot password?")}{" "}
+
+ {__("Reset password")}
+
+
+
+ );
+ }
+
+ if (mode === "password") {
+ return (
+
+ );
+ }
+
return (
-
);
}
diff --git a/apps/console/src/pages/auth/RegisterPage.tsx b/apps/console/src/pages/auth/RegisterPage.tsx
index 091e252ef..5b9d8d521 100644
--- a/apps/console/src/pages/auth/RegisterPage.tsx
+++ b/apps/console/src/pages/auth/RegisterPage.tsx
@@ -26,7 +26,7 @@ export default function RegisterPage() {
const onSubmit = handleSubmit(async (data) => {
const response = await fetch(
- buildEndpoint("/api/console/v1/auth/register"),
+ buildEndpoint("/auth/register"),
{
method: "POST",
headers: {
@@ -106,7 +106,7 @@ export default function RegisterPage() {
{__("Already have an account?")}{" "}
{__("Log in here")}
diff --git a/apps/console/src/pages/auth/ResetPasswordPage.tsx b/apps/console/src/pages/auth/ResetPasswordPage.tsx
index 3afa7f0d2..966b2d05a 100644
--- a/apps/console/src/pages/auth/ResetPasswordPage.tsx
+++ b/apps/console/src/pages/auth/ResetPasswordPage.tsx
@@ -44,7 +44,7 @@ export default function ResetPasswordPage() {
}
const response = await fetch(
- buildEndpoint("/api/console/v1/auth/reset-password"),
+ buildEndpoint("/auth/reset-password"),
{
method: "POST",
headers: {
@@ -74,7 +74,7 @@ export default function ResetPasswordPage() {
description: __("Password reset successfully"),
variant: "success",
});
- navigate("/auth/login", { replace: true });
+ navigate("/authentication/login", { replace: true });
});
usePageTitle(__("Reset password"));
@@ -118,7 +118,7 @@ export default function ResetPasswordPage() {
{__("Remember your password?")}{" "}
{__("Log in here")}
diff --git a/apps/console/src/pages/auth/SignupFromInvitationPage.tsx b/apps/console/src/pages/auth/SignupFromInvitationPage.tsx
index d03eeeca7..9a2804f06 100644
--- a/apps/console/src/pages/auth/SignupFromInvitationPage.tsx
+++ b/apps/console/src/pages/auth/SignupFromInvitationPage.tsx
@@ -51,7 +51,7 @@ export default function SignupFromInvitationPage() {
}
const response = await fetch(
- buildEndpoint("/api/console/v1/auth/signup-from-invitation"),
+ buildEndpoint("/auth/signup-from-invitation"),
{
method: "POST",
headers: {
@@ -125,7 +125,7 @@ export default function SignupFromInvitationPage() {
{__("Already have an account?")}{" "}
{__("Log in here")}
diff --git a/apps/console/src/pages/organizations/SettingsPage.tsx b/apps/console/src/pages/organizations/SettingsPage.tsx
index 3e4468ad7..103c2e970 100644
--- a/apps/console/src/pages/organizations/SettingsPage.tsx
+++ b/apps/console/src/pages/organizations/SettingsPage.tsx
@@ -1,213 +1,41 @@
-import {
- Avatar,
- Badge,
- Button,
- Card,
- Dialog,
- DialogContent,
- DialogFooter,
- Field,
- FileButton,
- IconTrashCan,
- Label,
- PageHeader,
- Spinner,
- TabBadge,
- TabItem,
- Tabs,
- Tbody,
- Td,
- Textarea,
- Th,
- Thead,
- Tr,
- useConfirm,
- useDialogRef,
-} from "@probo/ui";
+import { Outlet } from "react-router";
import { useTranslate } from "@probo/i18n";
import type { PreloadedQuery } from "react-relay";
import type { OrganizationGraph_ViewQuery } from "/hooks/graph/__generated__/OrganizationGraph_ViewQuery.graphql";
-import { useFragment, usePreloadedQuery, usePaginationFragment } from "react-relay";
+import { usePreloadedQuery } from "react-relay";
import { organizationViewQuery } from "/hooks/graph/OrganizationGraph";
-import { graphql } from "relay-runtime";
-import { SortableTable, SortableTh } from "/components/SortableTable";
-import clsx from "clsx";
-import type { SettingsPageFragment$key } from "./__generated__/SettingsPageFragment.graphql";
-import type {
- SettingsPageMembershipsFragment$data,
- SettingsPageMembershipsFragment$key
-} from "./__generated__/SettingsPageMembershipsFragment.graphql";
-import type {
- SettingsPageInvitationsFragment$data,
- SettingsPageInvitationsFragment$key
-} from "./__generated__/SettingsPageInvitationsFragment.graphql";
-import { useState, type ChangeEventHandler, useEffect, useRef } from "react";
-import { sprintf } from "@probo/helpers";
-import { useFormWithSchema } from "/hooks/useFormWithSchema";
-import { z } from "zod";
-import type { NodeOf } from "/types";
+import {
+ IconSettingsGear2,
+ IconPeopleAdd,
+ IconStore,
+ IconLock,
+ PageHeader,
+ TabLink,
+ Tabs,
+} from "@probo/ui";
import { useOrganizationId } from "/hooks/useOrganizationId";
-import { InviteUserDialog } from "/components/organizations/InviteUserDialog";
-import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
-import { useNavigate } from "react-router";
-import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
-import { CustomDomainManager } from "/components/customDomains/CustomDomainManager";
-import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
-
-const organizationSchema = z.object({
- name: z.string().min(1, "Organization name is required"),
- description: z.string().optional(),
- websiteUrl: z.string().optional(),
- email: z.string().optional(),
- headquarterAddress: z.string().optional(),
-});
-
-type OrganizationFormData = z.infer;
-
-type Props = {
- queryRef: PreloadedQuery;
-};
+import { graphql } from "relay-runtime";
+import type { SettingsPageFragment$key } from "./__generated__/SettingsPageFragment.graphql";
+import { useFragment } from "react-relay";
const organizationFragment = graphql`
fragment SettingsPageFragment on Organization {
id
name
- logoUrl
- horizontalLogoUrl
- description
- websiteUrl
- email
- headquarterAddress
- customDomain {
- id
- domain
- sslStatus
- dnsRecords {
- type
- name
- value
- ttl
- purpose
- }
- createdAt
- updatedAt
- sslExpiresAt
- }
- createdAt
- updatedAt
+ ...GeneralSettingsTabFragment
+ ...MembersSettingsTabMembershipsFragment
+ ...MembersSettingsTabInvitationsFragment
+ ...DomainSettingsTabFragment
+ ...SAMLSettingsTabFragment
}
`;
-const paginatedMembershipsFragment = graphql`
- fragment SettingsPageMembershipsFragment on Organization
- @refetchable(queryName: "SettingsMembershipsRefetchQuery")
- @argumentDefinitions(
- first: { type: "Int", defaultValue: 20 }
- order: { type: "MembershipOrder", defaultValue: { direction: ASC, field: CREATED_AT } }
- after: { type: "CursorKey", defaultValue: null }
- before: { type: "CursorKey", defaultValue: null }
- last: { type: "Int", defaultValue: null }
- ) {
- memberships(
- first: $first
- after: $after
- last: $last
- before: $before
- orderBy: $order
- ) @connection(key: "SettingsPageMemberships_memberships") {
- __id
- totalCount
- edges {
- node {
- id
- fullName
- emailAddress
- role
- createdAt
- }
- }
- }
- }
-`;
-
-const paginatedInvitationsFragment = graphql`
- fragment SettingsPageInvitationsFragment on Organization
- @refetchable(queryName: "SettingsInvitationsRefetchQuery")
- @argumentDefinitions(
- first: { type: "Int", defaultValue: 20 }
- order: { type: "InvitationOrder", defaultValue: { direction: ASC, field: CREATED_AT } }
- after: { type: "CursorKey", defaultValue: null }
- before: { type: "CursorKey", defaultValue: null }
- last: { type: "Int", defaultValue: null }
- ) {
- invitations(
- first: $first
- after: $after
- last: $last
- before: $before
- orderBy: $order
- filter: {statuses: [PENDING, EXPIRED]}
- ) @connection(key: "SettingsPageInvitations_invitations") {
- __id
- totalCount
- edges {
- node {
- id
- email
- fullName
- role
- status
- expiresAt
- acceptedAt
- createdAt
- }
- }
- }
- }
-`;
-
-const deleteInvitationMutation = graphql`
- mutation SettingsPage_DeleteInvitationMutation(
- $input: DeleteInvitationInput!
- $connections: [ID!]!
- ) {
- deleteInvitation(input: $input) {
- deletedInvitationId @deleteEdge(connections: $connections)
- }
- }
-`;
-
-const updateOrganizationMutation = graphql`
- mutation SettingsPage_UpdateMutation($input: UpdateOrganizationInput!) {
- updateOrganization(input: $input) {
- organization {
- id
- name
- logoUrl
- horizontalLogoUrl
- description
- websiteUrl
- email
- headquarterAddress
- }
- }
- }
-`;
-
-const deleteHorizontalLogoMutation = graphql`
- mutation SettingsPage_DeleteHorizontalLogoMutation($input: DeleteOrganizationHorizontalLogoInput!) {
- deleteOrganizationHorizontalLogo(input: $input) {
- organization {
- id
- horizontalLogoUrl
- }
- }
- }
-`;
+type Props = {
+ queryRef: PreloadedQuery;
+};
export default function SettingsPage({ queryRef }: Props) {
const { __ } = useTranslate();
- const navigate = useNavigate();
const organizationId = useOrganizationId();
const organizationKey = usePreloadedQuery(
organizationViewQuery,
@@ -218,683 +46,30 @@ export default function SettingsPage({ queryRef }: Props) {
organizationKey
);
- const membershipsPagination = usePaginationFragment(
- paginatedMembershipsFragment,
- organizationKey as SettingsPageMembershipsFragment$key
- );
-
- const invitationsPagination = usePaginationFragment(
- paginatedInvitationsFragment,
- organizationKey as SettingsPageInvitationsFragment$key
- );
-
- const refetchMemberships = () => {
- membershipsPagination.refetch({}, { fetchPolicy: 'network-only' });
- };
-
- const refetchInvitations = () => {
- invitationsPagination.refetch({}, { fetchPolicy: 'network-only' });
- };
-
- const [updateOrganization, isUpdatingOrganization] = useMutationWithToasts(
- updateOrganizationMutation,
- {
- successMessage: __("Organization updated successfully"),
- errorMessage: __("Failed to update organization"),
- }
- );
- const [deleteHorizontalLogo, isDeletingHorizontalLogo] = useMutationWithToasts(
- deleteHorizontalLogoMutation,
- {
- successMessage: __("Horizontal logo deleted successfully"),
- errorMessage: __("Failed to delete horizontal logo"),
- }
- );
- const [deleteOrganization, isDeleting] = useDeleteOrganizationMutation();
- const memberships = membershipsPagination.data.memberships?.edges.map((edge) => edge.node) || [];
- const invitations = invitationsPagination.data.invitations?.edges.map((edge) => edge.node) || [];
- const [activeTab, setActiveTab] = useState<"memberships" | "invitations">("memberships");
- const [logoPreview, setLogoPreview] = useState(null);
- const [horizontalLogoPreview, setHorizontalLogoPreview] = useState(null);
-
- const { formState, handleSubmit, register, reset } = useFormWithSchema(
- organizationSchema,
- {
- defaultValues: {
- name: organization.name || "",
- description: organization.description || "",
- websiteUrl: organization.websiteUrl || "",
- email: organization.email || "",
- headquarterAddress: organization.headquarterAddress || "",
- },
- }
- );
-
- const prevOrgDataRef = useRef({
- name: organization.name,
- description: organization.description,
- websiteUrl: organization.websiteUrl,
- email: organization.email,
- headquarterAddress: organization.headquarterAddress,
- });
-
- useEffect(() => {
- const prev = prevOrgDataRef.current;
- const hasFormFieldChanges =
- prev.name !== organization.name ||
- prev.description !== organization.description ||
- prev.websiteUrl !== organization.websiteUrl ||
- prev.email !== organization.email ||
- prev.headquarterAddress !== organization.headquarterAddress;
-
- if (hasFormFieldChanges) {
- reset({
- name: organization.name || "",
- description: organization.description || "",
- websiteUrl: organization.websiteUrl || "",
- email: organization.email || "",
- headquarterAddress: organization.headquarterAddress || "",
- });
- setLogoPreview(null);
- setHorizontalLogoPreview(null);
-
- prevOrgDataRef.current = {
- name: organization.name,
- description: organization.description,
- websiteUrl: organization.websiteUrl,
- email: organization.email,
- headquarterAddress: organization.headquarterAddress,
- };
- }
- }, [organization, reset]);
-
- const onSubmit = handleSubmit((data: OrganizationFormData) => {
- updateOrganization({
- variables: {
- input: {
- organizationId: organization.id,
- name: data.name,
- description: data.description || undefined,
- websiteUrl: data.websiteUrl || undefined,
- email: data.email || undefined,
- headquarterAddress: data.headquarterAddress || undefined,
- },
- },
- });
- });
-
- const handleLogoChange: ChangeEventHandler = (e) => {
- const file = e.target.files?.[0];
- if (!file) {
- return;
- }
-
- const reader = new FileReader();
- reader.onloadend = () => {
- setLogoPreview(reader.result as string);
- };
- reader.readAsDataURL(file);
-
- updateOrganization({
- variables: {
- input: {
- organizationId: organization.id,
- logoFile: null,
- },
- },
- uploadables: {
- "input.logoFile": file,
- },
- onSuccess: () => {
- setLogoPreview(null);
- },
- });
- };
-
- const handleHorizontalLogoChange: ChangeEventHandler = (e) => {
- const file = e.target.files?.[0];
- if (!file) {
- return;
- }
-
- const reader = new FileReader();
- reader.onloadend = () => {
- setHorizontalLogoPreview(reader.result as string);
- };
- reader.readAsDataURL(file);
-
- updateOrganization({
- variables: {
- input: {
- organizationId: organization.id,
- horizontalLogoFile: null,
- },
- },
- uploadables: {
- "input.horizontalLogoFile": file,
- },
- onSuccess: () => {
- setHorizontalLogoPreview(null);
- },
- });
- };
-
- const deleteDialogRef = useDialogRef();
-
- const handleDeleteHorizontalLogo = () => {
- deleteHorizontalLogo({
- variables: {
- input: {
- organizationId: organization.id,
- },
- },
- onSuccess: () => {
- deleteDialogRef.current?.close();
- },
- });
- };
-
- const handleDeleteOrganization = () => {
- return deleteOrganization({
- variables: {
- input: {
- organizationId: organization.id,
- },
- connections: [],
- },
- onSuccess: () => {
- navigate("/", { replace: true });
- },
- });
- };
-
return (
- {/* Organization settings */}
-
-
-
-
{__("Workspace members")}
-
- {__("Invite member")}
-
-
-
-
- setActiveTab("memberships")}
- >
- {__("Members")}
- {(membershipsPagination.data.memberships?.totalCount || 0) > 0 && (
- {membershipsPagination.data.memberships?.totalCount}
- )}
-
- setActiveTab("invitations")}
- >
- {__("Invitations")}
- {(invitationsPagination.data.invitations?.totalCount || 0) > 0 && (
- {invitationsPagination.data.invitations?.totalCount}
- )}
-
-
-
-
-
- {activeTab === "memberships" && (
- {
- membershipsPagination.refetch({
- order: {
- direction: order.direction as "ASC" | "DESC",
- field: order.field as "CREATED_AT" | "FULL_NAME" | "EMAIL_ADDRESS" | "ROLE"
- }
- });
- }}
- >
-
-
- {__("Name")}
- {__("Email")}
- {__("Role")}
- {__("Joined")}
- |
-
-
-
- {memberships.length === 0 ? (
-
- |
- {__("No members")}
- |
-
- ) : (
- memberships.map((membership) => (
-
- ))
- )}
-
-
- )}
-
- {activeTab === "invitations" && (
- {
- invitationsPagination.refetch({
- order: {
- direction: order.direction as "ASC" | "DESC",
- field: order.field as "CREATED_AT" | "EXPIRES_AT" | "FULL_NAME" | "EMAIL" | "ROLE" | "STATUS" | "ACCEPTED_AT"
- }
- });
- }}
- >
-
-
- {__("Name")}
- {__("Email")}
- {__("Role")}
- {__("Invited")}
- | {__("Status")} |
- {__("Accepted at")}
- |
-
-
-
- {invitations.length === 0 ? (
-
- |
- {__("No invitations")}
- |
-
- ) : (
- invitations.map((invitation) => (
-
- ))
- )}
-
-
- )}
-
-
-
-
-
-
{__("Custom Domain")}
-
-
-
-
-
- {__("Danger Zone")}
-
-
-
-
- {__("Delete Organization")}
-
-
- {__("Permanently delete this organization and all its data.")}{" "}
-
- {__("This action cannot be undone.")}
-
-
-
-
-
- {isDeleting ? __("Deleting...") : __("Delete Organization")}
-
-
-
-
+
);
}
-
-const removeMemberMutation = graphql`
- mutation SettingsPage_RemoveMemberMutation(
- $input: RemoveMemberInput!
- $connections: [ID!]!
- ) {
- removeMember(input: $input) {
- deletedMemberId @deleteEdge(connections: $connections)
- }
- }
-`;
-
-function InvitationRow(props: {
- invitation: NodeOf;
- connectionId?: string;
- organizationId: string;
- onRefetch: () => void;
-}) {
- const { __ } = useTranslate();
- const confirm = useConfirm();
- const [deleteInvitation, isDeleting] = useMutationWithToasts(
- deleteInvitationMutation,
- {
- successMessage: __("Invitation deleted successfully"),
- errorMessage: __("Failed to delete invitation"),
- }
- );
-
- const onDelete = () => {
- confirm(
- () => {
- return deleteInvitation({
- variables: {
- input: {
- invitationId: props.invitation.id,
- },
- connections: props.connectionId ? [props.connectionId] : [],
- },
- onCompleted: () => {
- props.onRefetch();
- },
- });
- },
- {
- message: sprintf(
- __("Are you sure you want to delete the invitation for %s?"),
- props.invitation.fullName
- ),
- }
- );
- };
-
- return (
-
- |
- {props.invitation.fullName}
- |
- {props.invitation.email} |
-
- {props.invitation.role}
- |
- {new Date(props.invitation.createdAt).toLocaleDateString()} |
-
- {props.invitation.status === "ACCEPTED" ? (
- {__("Accepted")}
- ) : props.invitation.status === "EXPIRED" ? (
- {__("Expired")}
- ) : (
- {__("Pending")}
- )}
- |
-
- {props.invitation.acceptedAt ? new Date(props.invitation.acceptedAt).toLocaleDateString() : "-"}
- |
-
- e.stopPropagation()}
- >
- {isDeleting ? (
-
- ) : (
-
- )}
-
- |
-
- );
-}
-
-function MembershipRow(props: {
- membership: NodeOf;
- connectionId?: string;
- organizationId: string;
- onRefetch: () => void;
-}) {
- const { __ } = useTranslate();
- const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
- successMessage: __("Member removed successfully"),
- errorMessage: __("Failed to remove member"),
- });
- const confirm = useConfirm();
- const [isRemoved, setIsRemoved] = useState(false);
-
- if (isRemoved) {
- return null;
- }
-
- const onRemove = async () => {
- confirm(
- () => {
- return removeMember({
- variables: {
- input: {
- memberId: props.membership.id,
- organizationId: props.organizationId,
- },
- connections: props.connectionId ? [props.connectionId] : [],
- },
- onCompleted: () => {
- setIsRemoved(true);
- props.onRefetch();
- },
- });
- },
- {
- message: sprintf(
- __("Are you sure you want to remove %s?"),
- props.membership.fullName
- ),
- }
- );
- };
-
- return (
-
- |
- {props.membership.fullName}
- |
- {props.membership.emailAddress} |
-
- {props.membership.role}
- |
- {new Date(props.membership.createdAt).toLocaleDateString()} |
-
- e.stopPropagation()}
- >
- {isRemoving ? (
-
- ) : (
-
- )}
-
- |
-
- );
-}
diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts b/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts
index 20fabeda0..fca5bb3e0 100644
--- a/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts
+++ b/apps/console/src/pages/organizations/__generated__/SettingsPageFragment.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<>
+ * @generated SignedSource<<552529ec3c732ce161f2dcb4a16179b9>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,34 +9,11 @@
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
-export type SSLStatus = "ACTIVE" | "EXPIRED" | "FAILED" | "PENDING" | "PROVISIONING" | "RENEWING";
import { FragmentRefs } from "relay-runtime";
export type SettingsPageFragment$data = {
- readonly createdAt: any;
- readonly customDomain: {
- readonly createdAt: any;
- readonly dnsRecords: ReadonlyArray<{
- readonly name: string;
- readonly purpose: string;
- readonly ttl: number;
- readonly type: string;
- readonly value: string;
- }>;
- readonly domain: string;
- readonly id: string;
- readonly sslExpiresAt: any | null | undefined;
- readonly sslStatus: SSLStatus;
- readonly updatedAt: any;
- } | null | undefined;
- readonly description: string | null | undefined;
- readonly email: string | null | undefined;
- readonly headquarterAddress: string | null | undefined;
- readonly horizontalLogoUrl: string | null | undefined;
readonly id: string;
- readonly logoUrl: string | null | undefined;
readonly name: string;
- readonly updatedAt: any;
- readonly websiteUrl: string | null | undefined;
+ readonly " $fragmentSpreads": FragmentRefs<"DomainSettingsTabFragment" | "GeneralSettingsTabFragment" | "MembersSettingsTabInvitationsFragment" | "MembersSettingsTabMembershipsFragment" | "SAMLSettingsTabFragment">;
readonly " $fragmentType": "SettingsPageFragment";
};
export type SettingsPageFragment$key = {
@@ -44,168 +21,56 @@ export type SettingsPageFragment$key = {
readonly " $fragmentSpreads": FragmentRefs<"SettingsPageFragment">;
};
-const node: ReaderFragment = (function(){
-var v0 = {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "id",
- "storageKey": null
-},
-v1 = {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "name",
- "storageKey": null
-},
-v2 = {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "createdAt",
- "storageKey": null
-},
-v3 = {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "updatedAt",
- "storageKey": null
-};
-return {
+const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "SettingsPageFragment",
"selections": [
- (v0/*: any*/),
- (v1/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
- "name": "logoUrl",
+ "name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
- "name": "horizontalLogoUrl",
+ "name": "name",
"storageKey": null
},
{
- "alias": null,
"args": null,
- "kind": "ScalarField",
- "name": "description",
- "storageKey": null
+ "kind": "FragmentSpread",
+ "name": "GeneralSettingsTabFragment"
},
{
- "alias": null,
"args": null,
- "kind": "ScalarField",
- "name": "websiteUrl",
- "storageKey": null
+ "kind": "FragmentSpread",
+ "name": "MembersSettingsTabMembershipsFragment"
},
{
- "alias": null,
"args": null,
- "kind": "ScalarField",
- "name": "email",
- "storageKey": null
+ "kind": "FragmentSpread",
+ "name": "MembersSettingsTabInvitationsFragment"
},
{
- "alias": null,
"args": null,
- "kind": "ScalarField",
- "name": "headquarterAddress",
- "storageKey": null
+ "kind": "FragmentSpread",
+ "name": "DomainSettingsTabFragment"
},
{
- "alias": null,
"args": null,
- "concreteType": "CustomDomain",
- "kind": "LinkedField",
- "name": "customDomain",
- "plural": false,
- "selections": [
- (v0/*: any*/),
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "domain",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "sslStatus",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "concreteType": "DNSRecordInstruction",
- "kind": "LinkedField",
- "name": "dnsRecords",
- "plural": true,
- "selections": [
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "type",
- "storageKey": null
- },
- (v1/*: any*/),
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "value",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "ttl",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "purpose",
- "storageKey": null
- }
- ],
- "storageKey": null
- },
- (v2/*: any*/),
- (v3/*: any*/),
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "sslExpiresAt",
- "storageKey": null
- }
- ],
- "storageKey": null
- },
- (v2/*: any*/),
- (v3/*: any*/)
+ "kind": "FragmentSpread",
+ "name": "SAMLSettingsTabFragment"
+ }
],
"type": "Organization",
"abstractKey": null
};
-})();
-(node as any).hash = "1e64bcccf8ef3a8ead79b3446e8a3ccd";
+(node as any).hash = "4f0ec089ac8ee79935eb56c22de31eca";
export default node;
diff --git a/apps/console/src/pages/organizations/settings/DomainSettingsTab.tsx b/apps/console/src/pages/organizations/settings/DomainSettingsTab.tsx
new file mode 100644
index 000000000..84dd01917
--- /dev/null
+++ b/apps/console/src/pages/organizations/settings/DomainSettingsTab.tsx
@@ -0,0 +1,46 @@
+import { useOutletContext } from "react-router";
+import { useFragment, graphql } from "react-relay";
+import { useTranslate } from "@probo/i18n";
+import { CustomDomainManager } from "/components/customDomains/CustomDomainManager";
+import type { DomainSettingsTabFragment$key } from "./__generated__/DomainSettingsTabFragment.graphql";
+
+const domainSettingsTabFragment = graphql`
+ fragment DomainSettingsTabFragment on Organization {
+ id
+ customDomain {
+ id
+ domain
+ sslStatus
+ dnsRecords {
+ type
+ name
+ value
+ ttl
+ purpose
+ }
+ createdAt
+ updatedAt
+ sslExpiresAt
+ }
+ }
+`;
+
+type OutletContext = {
+ organization: DomainSettingsTabFragment$key;
+};
+
+export default function DomainSettingsTab() {
+ const { __ } = useTranslate();
+ const { organization: organizationKey } = useOutletContext();
+ const organization = useFragment(domainSettingsTabFragment, organizationKey);
+
+ return (
+
+
{__("Custom Domain")}
+
+
+ );
+}
diff --git a/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx b/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx
new file mode 100644
index 000000000..8792b5e34
--- /dev/null
+++ b/apps/console/src/pages/organizations/settings/GeneralSettingsTab.tsx
@@ -0,0 +1,444 @@
+import { useState, useRef, useEffect, type ChangeEventHandler } from "react";
+import { useOutletContext, useNavigate } from "react-router";
+import { useFragment, graphql } from "react-relay";
+import {
+ Avatar,
+ Button,
+ Card,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ Field,
+ FileButton,
+ IconTrashCan,
+ Label,
+ Spinner,
+ Textarea,
+ useDialogRef,
+} from "@probo/ui";
+import { useTranslate } from "@probo/i18n";
+import { useFormWithSchema } from "/hooks/useFormWithSchema";
+import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
+import { z } from "zod";
+import type { GeneralSettingsTabFragment$key } from "./__generated__/GeneralSettingsTabFragment.graphql";
+import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
+import { useDeleteOrganizationMutation } from "/hooks/graph/OrganizationGraph";
+
+const generalSettingsTabFragment = graphql`
+ fragment GeneralSettingsTabFragment on Organization {
+ id
+ name
+ logoUrl
+ horizontalLogoUrl
+ description
+ websiteUrl
+ email
+ headquarterAddress
+ createdAt
+ updatedAt
+ }
+`;
+
+const updateOrganizationMutation = graphql`
+ mutation GeneralSettingsTab_UpdateMutation($input: UpdateOrganizationInput!) {
+ updateOrganization(input: $input) {
+ organization {
+ id
+ name
+ logoUrl
+ horizontalLogoUrl
+ description
+ websiteUrl
+ email
+ headquarterAddress
+ }
+ }
+ }
+`;
+
+const deleteHorizontalLogoMutation = graphql`
+ mutation GeneralSettingsTab_DeleteHorizontalLogoMutation(
+ $input: DeleteOrganizationHorizontalLogoInput!
+ ) {
+ deleteOrganizationHorizontalLogo(input: $input) {
+ organization {
+ id
+ horizontalLogoUrl
+ }
+ }
+ }
+`;
+
+const organizationSchema = z.object({
+ name: z.string().min(1, "Organization name is required"),
+ description: z.string().optional(),
+ websiteUrl: z.string().optional(),
+ email: z.string().optional(),
+ headquarterAddress: z.string().optional(),
+});
+
+type OrganizationFormData = z.infer;
+
+type OutletContext = {
+ organization: GeneralSettingsTabFragment$key;
+};
+
+export default function GeneralSettingsTab() {
+ const { __ } = useTranslate();
+ const navigate = useNavigate();
+ const { organization: organizationKey } = useOutletContext();
+ const organization = useFragment(generalSettingsTabFragment, organizationKey);
+ const deleteDialogRef = useDialogRef();
+
+ const [logoPreview, setLogoPreview] = useState(null);
+ const [horizontalLogoPreview, setHorizontalLogoPreview] = useState<
+ string | null
+ >(null);
+
+ const [updateOrganization, isUpdatingOrganization] = useMutationWithToasts(
+ updateOrganizationMutation,
+ {
+ successMessage: __("Organization updated successfully"),
+ errorMessage: __("Failed to update organization"),
+ }
+ );
+ const [deleteHorizontalLogo, isDeletingHorizontalLogo] =
+ useMutationWithToasts(deleteHorizontalLogoMutation, {
+ successMessage: __("Horizontal logo deleted successfully"),
+ errorMessage: __("Failed to delete horizontal logo"),
+ });
+ const [deleteOrganization, isDeletingOrganization] =
+ useDeleteOrganizationMutation();
+
+ const { formState, handleSubmit, register, reset } = useFormWithSchema(
+ organizationSchema,
+ {
+ defaultValues: {
+ name: organization.name || "",
+ description: organization.description || "",
+ websiteUrl: organization.websiteUrl || "",
+ email: organization.email || "",
+ headquarterAddress: organization.headquarterAddress || "",
+ },
+ }
+ );
+
+ const prevOrgDataRef = useRef({
+ name: organization.name,
+ description: organization.description,
+ websiteUrl: organization.websiteUrl,
+ email: organization.email,
+ headquarterAddress: organization.headquarterAddress,
+ });
+
+ useEffect(() => {
+ const prevData = prevOrgDataRef.current;
+ const currentData = {
+ name: organization.name,
+ description: organization.description,
+ websiteUrl: organization.websiteUrl,
+ email: organization.email,
+ headquarterAddress: organization.headquarterAddress,
+ };
+
+ if (JSON.stringify(prevData) !== JSON.stringify(currentData)) {
+ reset({
+ name: organization.name || "",
+ description: organization.description || "",
+ websiteUrl: organization.websiteUrl || "",
+ email: organization.email || "",
+ headquarterAddress: organization.headquarterAddress || "",
+ });
+ prevOrgDataRef.current = currentData;
+ }
+ }, [organization, reset]);
+
+ const onSubmit = handleSubmit((data: OrganizationFormData) => {
+ updateOrganization({
+ variables: {
+ input: {
+ organizationId: organization.id,
+ name: data.name,
+ description: data.description || null,
+ websiteUrl: data.websiteUrl || null,
+ email: data.email || null,
+ headquarterAddress: data.headquarterAddress || null,
+ },
+ },
+ });
+ });
+
+ const handleLogoChange: ChangeEventHandler = (e) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = () => {
+ setLogoPreview(reader.result as string);
+ updateOrganization({
+ variables: {
+ input: {
+ organizationId: organization.id,
+ logo: file,
+ },
+ },
+ onCompleted: () => {
+ setLogoPreview(null);
+ },
+ });
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const handleHorizontalLogoChange: ChangeEventHandler = (
+ e
+ ) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.onload = () => {
+ setHorizontalLogoPreview(reader.result as string);
+ updateOrganization({
+ variables: {
+ input: {
+ organizationId: organization.id,
+ horizontalLogo: file,
+ },
+ },
+ onCompleted: () => {
+ setHorizontalLogoPreview(null);
+ },
+ });
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const handleDeleteHorizontalLogo = () => {
+ deleteHorizontalLogo({
+ variables: {
+ input: {
+ organizationId: organization.id,
+ },
+ },
+ onCompleted: () => {
+ deleteDialogRef.current?.close();
+ },
+ });
+ };
+
+ const handleDeleteOrganization = () => {
+ return deleteOrganization({
+ variables: {
+ input: {
+ organizationId: organization.id,
+ },
+ connections: [],
+ },
+ onSuccess: () => {
+ navigate("/", { replace: true });
+ },
+ });
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/console/src/pages/organizations/settings/MembersSettingsTab.tsx b/apps/console/src/pages/organizations/settings/MembersSettingsTab.tsx
new file mode 100644
index 000000000..949efbdac
--- /dev/null
+++ b/apps/console/src/pages/organizations/settings/MembersSettingsTab.tsx
@@ -0,0 +1,449 @@
+import { useState } from "react";
+import { useOutletContext } from "react-router";
+import { usePaginationFragment, graphql } from "react-relay";
+import {
+ Badge,
+ Button,
+ Card,
+ IconTrashCan,
+ Spinner,
+ TabBadge,
+ TabItem,
+ Tabs,
+ Tbody,
+ Td,
+ Th,
+ Thead,
+ Tr,
+ useConfirm,
+} from "@probo/ui";
+import { useTranslate } from "@probo/i18n";
+import { SortableTable, SortableTh } from "/components/SortableTable";
+import { InviteUserDialog } from "/components/organizations/InviteUserDialog";
+import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
+import { sprintf } from "@probo/helpers";
+import clsx from "clsx";
+import type { NodeOf } from "/types";
+import type {
+ MembersSettingsTabMembershipsFragment$data,
+ MembersSettingsTabMembershipsFragment$key
+} from "./__generated__/MembersSettingsTabMembershipsFragment.graphql";
+import type {
+ MembersSettingsTabInvitationsFragment$data,
+ MembersSettingsTabInvitationsFragment$key
+} from "./__generated__/MembersSettingsTabInvitationsFragment.graphql";
+
+const paginatedMembershipsFragment = graphql`
+ fragment MembersSettingsTabMembershipsFragment on Organization
+ @refetchable(queryName: "MembersSettingsTabMembershipsRefetchQuery")
+ @argumentDefinitions(
+ first: { type: "Int", defaultValue: 20 }
+ order: { type: "MembershipOrder", defaultValue: { direction: ASC, field: CREATED_AT } }
+ after: { type: "CursorKey", defaultValue: null }
+ before: { type: "CursorKey", defaultValue: null }
+ last: { type: "Int", defaultValue: null }
+ ) {
+ memberships(
+ first: $first
+ after: $after
+ last: $last
+ before: $before
+ orderBy: $order
+ ) @connection(key: "MembersSettingsTabMemberships_memberships") {
+ __id
+ totalCount
+ edges {
+ node {
+ id
+ fullName
+ emailAddress
+ role
+ authMethod
+ createdAt
+ }
+ }
+ }
+ }
+`;
+
+const paginatedInvitationsFragment = graphql`
+ fragment MembersSettingsTabInvitationsFragment on Organization
+ @refetchable(queryName: "MembersSettingsTabInvitationsRefetchQuery")
+ @argumentDefinitions(
+ first: { type: "Int", defaultValue: 20 }
+ order: { type: "InvitationOrder", defaultValue: { direction: ASC, field: CREATED_AT } }
+ after: { type: "CursorKey", defaultValue: null }
+ before: { type: "CursorKey", defaultValue: null }
+ last: { type: "Int", defaultValue: null }
+ ) {
+ invitations(
+ first: $first
+ after: $after
+ last: $last
+ before: $before
+ orderBy: $order
+ ) @connection(key: "MembersSettingsTabInvitations_invitations") {
+ __id
+ totalCount
+ edges {
+ node {
+ id
+ fullName
+ email
+ role
+ status
+ createdAt
+ expiresAt
+ acceptedAt
+ }
+ }
+ }
+ }
+`;
+
+const removeMemberMutation = graphql`
+ mutation MembersSettingsTab_RemoveMemberMutation(
+ $input: RemoveMemberInput!
+ $connections: [ID!]!
+ ) {
+ removeMember(input: $input) {
+ deletedMemberId @deleteEdge(connections: $connections)
+ }
+ }
+`;
+
+const deleteInvitationMutation = graphql`
+ mutation MembersSettingsTab_DeleteInvitationMutation(
+ $input: DeleteInvitationInput!
+ $connections: [ID!]!
+ ) {
+ deleteInvitation(input: $input) {
+ deletedInvitationId @deleteEdge(connections: $connections)
+ }
+ }
+`;
+
+type OutletContext = {
+ organization: MembersSettingsTabMembershipsFragment$key & MembersSettingsTabInvitationsFragment$key & { id: string };
+};
+
+export default function MembersSettingsTab() {
+ const { __ } = useTranslate();
+ const { organization: organizationKey } = useOutletContext();
+
+ const membershipsPagination = usePaginationFragment(
+ paginatedMembershipsFragment,
+ organizationKey as MembersSettingsTabMembershipsFragment$key
+ );
+
+ const invitationsPagination = usePaginationFragment(
+ paginatedInvitationsFragment,
+ organizationKey as MembersSettingsTabInvitationsFragment$key
+ );
+
+ const refetchMemberships = () => {
+ membershipsPagination.refetch({}, { fetchPolicy: 'network-only' });
+ };
+
+ const refetchInvitations = () => {
+ invitationsPagination.refetch({}, { fetchPolicy: 'network-only' });
+ };
+
+ const memberships = membershipsPagination.data.memberships?.edges.map((edge) => edge.node) || [];
+ const invitations = invitationsPagination.data.invitations?.edges.map((edge) => edge.node) || [];
+ const [activeTab, setActiveTab] = useState<"memberships" | "invitations">("memberships");
+
+ return (
+
+
+
{__("Workspace members")}
+
+ {__("Invite member")}
+
+
+
+
+ setActiveTab("memberships")}
+ >
+ {__("Members")}
+ {(membershipsPagination.data.memberships?.totalCount || 0) > 0 && (
+ {membershipsPagination.data.memberships?.totalCount}
+ )}
+
+ setActiveTab("invitations")}
+ >
+ {__("Invitations")}
+ {(invitationsPagination.data.invitations?.totalCount || 0) > 0 && (
+ {invitationsPagination.data.invitations?.totalCount}
+ )}
+
+
+
+
+
+ {activeTab === "memberships" && (
+ {
+ membershipsPagination.refetch({
+ order: {
+ direction: order.direction as "ASC" | "DESC",
+ field: order.field as "CREATED_AT" | "FULL_NAME" | "EMAIL_ADDRESS" | "ROLE"
+ }
+ });
+ }}
+ >
+
+
+ {__("Name")}
+ {__("Email")}
+ {__("Role")}
+ {__("Joined")}
+ |
+
+
+
+ {memberships.length === 0 ? (
+
+ |
+ {__("No members")}
+ |
+
+ ) : (
+ memberships.map((membership) => (
+
+ ))
+ )}
+
+
+ )}
+
+ {activeTab === "invitations" && (
+ {
+ invitationsPagination.refetch({
+ order: {
+ direction: order.direction as "ASC" | "DESC",
+ field: order.field as "CREATED_AT" | "EXPIRES_AT" | "FULL_NAME" | "EMAIL" | "ROLE" | "STATUS" | "ACCEPTED_AT"
+ }
+ });
+ }}
+ >
+
+
+ {__("Name")}
+ {__("Email")}
+ {__("Role")}
+ {__("Invited")}
+ | {__("Status")} |
+ {__("Accepted at")}
+ |
+
+
+
+ {invitations.length === 0 ? (
+
+ |
+ {__("No invitations")}
+ |
+
+ ) : (
+ invitations.map((invitation) => (
+
+ ))
+ )}
+
+
+ )}
+
+
+
+ );
+}
+
+function InvitationRow(props: {
+ invitation: NodeOf;
+ connectionId?: string;
+ organizationId: string;
+ onRefetch: () => void;
+}) {
+ const { __ } = useTranslate();
+ const confirm = useConfirm();
+ const [deleteInvitation, isDeleting] = useMutationWithToasts(
+ deleteInvitationMutation,
+ {
+ successMessage: __("Invitation deleted successfully"),
+ errorMessage: __("Failed to delete invitation"),
+ }
+ );
+
+ const onDelete = () => {
+ confirm(
+ () => {
+ return deleteInvitation({
+ variables: {
+ input: {
+ invitationId: props.invitation.id,
+ },
+ connections: props.connectionId ? [props.connectionId] : [],
+ },
+ onCompleted: () => {
+ props.onRefetch();
+ },
+ });
+ },
+ {
+ message: sprintf(
+ __("Are you sure you want to delete the invitation for %s?"),
+ props.invitation.fullName
+ ),
+ }
+ );
+ };
+
+ return (
+
+ |
+ {props.invitation.fullName}
+ |
+ {props.invitation.email} |
+
+ {props.invitation.role}
+ |
+ {new Date(props.invitation.createdAt).toLocaleDateString()} |
+
+ {props.invitation.status === "ACCEPTED" ? (
+ {__("Accepted")}
+ ) : props.invitation.status === "EXPIRED" ? (
+ {__("Expired")}
+ ) : (
+ {__("Pending")}
+ )}
+ |
+
+ {props.invitation.acceptedAt ? new Date(props.invitation.acceptedAt).toLocaleDateString() : "-"}
+ |
+
+ e.stopPropagation()}
+ >
+ {isDeleting ? (
+
+ ) : (
+
+ )}
+
+ |
+
+ );
+}
+
+function MembershipRow(props: {
+ membership: NodeOf;
+ connectionId?: string;
+ organizationId: string;
+ onRefetch: () => void;
+}) {
+ const { __ } = useTranslate();
+ const [removeMember, isRemoving] = useMutationWithToasts(removeMemberMutation, {
+ successMessage: __("Member removed successfully"),
+ errorMessage: __("Failed to remove member"),
+ });
+ const confirm = useConfirm();
+ const [isRemoved, setIsRemoved] = useState(false);
+
+ if (isRemoved) {
+ return null;
+ }
+
+ const onRemove = async () => {
+ confirm(
+ () => {
+ return removeMember({
+ variables: {
+ input: {
+ memberId: props.membership.id,
+ organizationId: props.organizationId,
+ },
+ connections: props.connectionId ? [props.connectionId] : [],
+ },
+ onCompleted: () => {
+ setIsRemoved(true);
+ props.onRefetch();
+ },
+ });
+ },
+ {
+ message: sprintf(
+ __("Are you sure you want to remove %s?"),
+ props.membership.fullName
+ ),
+ }
+ );
+ };
+
+ return (
+
+ |
+ {props.membership.fullName}
+ |
+
+
+ {props.membership.emailAddress}
+ {props.membership.authMethod === "SAML" && (
+ SAML
+ )}
+
+ |
+
+ {props.membership.role}
+ |
+ {new Date(props.membership.createdAt).toLocaleDateString()} |
+
+ e.stopPropagation()}
+ >
+ {isRemoving ? (
+
+ ) : (
+
+ )}
+
+ |
+
+ );
+}
diff --git a/apps/console/src/pages/organizations/settings/SAMLSettingsTab.tsx b/apps/console/src/pages/organizations/settings/SAMLSettingsTab.tsx
new file mode 100644
index 000000000..9154eac22
--- /dev/null
+++ b/apps/console/src/pages/organizations/settings/SAMLSettingsTab.tsx
@@ -0,0 +1,783 @@
+import { useState, useEffect } from "react";
+import { useOutletContext } from "react-router";
+import { useFragment, graphql } from "react-relay";
+import { Controller } from "react-hook-form";
+import { z } from "zod";
+import {
+ Button,
+ Card,
+ Checkbox,
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogTitle,
+ Field,
+ Label,
+ Option,
+ Select,
+ Table,
+ Tbody,
+ Td,
+ Textarea,
+ Th,
+ Thead,
+ Tr,
+ useConfirm,
+ useDialogRef,
+} from "@probo/ui";
+import { useTranslate } from "@probo/i18n";
+import { useFormWithSchema } from "/hooks/useFormWithSchema";
+import {
+ useCreateSAMLConfigurationMutation,
+ useUpdateSAMLConfigurationMutation,
+ useDeleteSAMLConfigurationMutation,
+ useEnableSAMLMutation,
+ useDisableSAMLMutation,
+ useInitiateDomainVerificationMutation,
+ useVerifyDomainMutation,
+} from "/hooks/graph/SAMLConfigurationGraph";
+import type { SAMLSettingsTabFragment$key } from "./__generated__/SAMLSettingsTabFragment.graphql";
+
+const samlSettingsTabFragment = graphql`
+ fragment SAMLSettingsTabFragment on Organization {
+ id
+ name
+ samlConfigurations {
+ id
+ enabled
+ emailDomain
+ enforcementPolicy
+ domainVerified
+ domainVerificationToken
+ domainVerifiedAt
+ spEntityId
+ spAcsUrl
+ spMetadataUrl
+ testLoginUrl
+ idpEntityId
+ idpSsoUrl
+ idpCertificate
+ idpMetadataUrl
+ attributeEmail
+ attributeFirstname
+ attributeLastname
+ attributeRole
+ defaultRole
+ autoSignupEnabled
+ }
+ }
+`;
+
+const initiateSchema = z.object({
+ emailDomain: z.string().min(1, "Email domain is required").regex(/^[a-z0-9.-]+\.[a-z]{2,}$/i, "Must be a valid domain (e.g., example.com)"),
+});
+
+const samlConfigSchema = z.object({
+ emailDomain: z.string().min(1, "Email domain is required").regex(/^[a-z0-9.-]+\.[a-z]{2,}$/i, "Must be a valid domain (e.g., example.com)"),
+ enforcementPolicy: z.enum(["OFF", "OPTIONAL", "REQUIRED"]),
+ spCertificate: z.string().optional(),
+ spPrivateKey: z.string().optional(),
+ idpEntityId: z.string().min(1, "IdP Entity ID is required"),
+ idpSsoUrl: z.string().url("IdP SSO URL must be a valid URL"),
+ idpCertificate: z.string().min(1, "IdP Certificate is required"),
+ idpMetadataUrl: z.string().url("IdP Metadata URL must be a valid URL").optional().or(z.literal("")),
+ attributeEmail: z.string().optional(),
+ attributeFirstname: z.string().optional(),
+ attributeLastname: z.string().optional(),
+ attributeRole: z.string().optional(),
+ defaultRole: z.string().optional(),
+ autoSignupEnabled: z.boolean().default(false),
+});
+
+type OutletContext = {
+ organization: SAMLSettingsTabFragment$key;
+};
+
+type SetupStep = "initiate" | "verify" | "configure";
+
+export default function SAMLSettingsTab() {
+ const { __ } = useTranslate();
+ const { organization: organizationKey } = useOutletContext();
+ const organization = useFragment(samlSettingsTabFragment, organizationKey);
+ const configs = organization.samlConfigurations;
+
+ const dialogRef = useDialogRef();
+ const [editingConfig, setEditingConfig] = useState(null);
+ const [currentStep, setCurrentStep] = useState("initiate");
+ const [dnsRecord, setDnsRecord] = useState("");
+
+ const [createMutation, isCreating] = useCreateSAMLConfigurationMutation();
+ const [updateMutation, isUpdating] = useUpdateSAMLConfigurationMutation();
+ const [deleteMutation] = useDeleteSAMLConfigurationMutation();
+ const [enableMutation, isEnabling] = useEnableSAMLMutation();
+ const [disableMutation, isDisabling] = useDisableSAMLMutation();
+ const [initiateDomainMutation, isInitiating] = useInitiateDomainVerificationMutation();
+ const [verifyDomainMutation, isVerifying] = useVerifyDomainMutation();
+
+ const confirm = useConfirm();
+
+ const handleOpenModal = (config?: typeof configs[0]) => {
+ setEditingConfig(config || null);
+ if (config) {
+ if (!config.domainVerified) {
+ setCurrentStep("verify");
+ setDnsRecord(`probo-verification=${config.domainVerificationToken}`);
+ } else {
+ setCurrentStep("configure");
+ }
+ } else {
+ setCurrentStep("initiate");
+ }
+ dialogRef.current?.open();
+ };
+
+ const handleCloseModal = () => {
+ setEditingConfig(null);
+ setCurrentStep("initiate");
+ setDnsRecord("");
+ dialogRef.current?.close();
+ };
+
+ const initiateForm = useFormWithSchema(initiateSchema, {
+ defaultValues: {
+ emailDomain: editingConfig?.emailDomain || "",
+ },
+ });
+
+ const form = useFormWithSchema(samlConfigSchema, {
+ defaultValues: editingConfig
+ ? {
+ emailDomain: editingConfig.emailDomain || "",
+ enforcementPolicy: editingConfig.enforcementPolicy || "OPTIONAL",
+ idpEntityId: editingConfig.idpEntityId || "",
+ idpSsoUrl: editingConfig.idpSsoUrl || "",
+ idpCertificate: editingConfig.idpCertificate || "",
+ idpMetadataUrl: editingConfig.idpMetadataUrl || "",
+ attributeEmail: editingConfig.attributeEmail || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+ attributeFirstname: editingConfig.attributeFirstname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
+ attributeLastname: editingConfig.attributeLastname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
+ attributeRole: editingConfig.attributeRole || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
+ defaultRole: editingConfig.defaultRole || "MEMBER",
+ autoSignupEnabled: editingConfig.autoSignupEnabled || false,
+ }
+ : {
+ emailDomain: "",
+ enforcementPolicy: "OPTIONAL",
+ idpEntityId: "",
+ idpSsoUrl: "",
+ idpCertificate: "",
+ idpMetadataUrl: "",
+ attributeEmail: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+ attributeFirstname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
+ attributeLastname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
+ attributeRole: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
+ defaultRole: "MEMBER",
+ autoSignupEnabled: false,
+ },
+ });
+
+ useEffect(() => {
+ if (editingConfig) {
+ form.reset({
+ emailDomain: editingConfig.emailDomain || "",
+ enforcementPolicy: editingConfig.enforcementPolicy || "OPTIONAL",
+ idpEntityId: editingConfig.idpEntityId || "",
+ idpSsoUrl: editingConfig.idpSsoUrl || "",
+ idpCertificate: editingConfig.idpCertificate || "",
+ idpMetadataUrl: editingConfig.idpMetadataUrl || "",
+ attributeEmail: editingConfig.attributeEmail || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+ attributeFirstname: editingConfig.attributeFirstname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
+ attributeLastname: editingConfig.attributeLastname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
+ attributeRole: editingConfig.attributeRole || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
+ defaultRole: editingConfig.defaultRole || "MEMBER",
+ autoSignupEnabled: editingConfig.autoSignupEnabled || false,
+ });
+ initiateForm.reset({
+ emailDomain: editingConfig.emailDomain || "",
+ });
+ } else {
+ form.reset({
+ emailDomain: "",
+ enforcementPolicy: "OPTIONAL",
+ idpEntityId: "",
+ idpSsoUrl: "",
+ idpCertificate: "",
+ idpMetadataUrl: "",
+ attributeEmail: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+ attributeFirstname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
+ attributeLastname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
+ attributeRole: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
+ defaultRole: "MEMBER",
+ autoSignupEnabled: false,
+ });
+ initiateForm.reset({
+ emailDomain: "",
+ });
+ }
+ }, [editingConfig, form, initiateForm]);
+
+ const handleInitiateDomain = initiateForm.handleSubmit((data) => {
+ initiateDomainMutation({
+ variables: {
+ input: {
+ organizationId: organization.id,
+ emailDomain: data.emailDomain,
+ },
+ },
+ onCompleted: (response) => {
+ setDnsRecord(response.initiateDomainVerification.dnsRecord);
+ setEditingConfig(response.initiateDomainVerification.samlConfiguration as any);
+ setCurrentStep("verify");
+ },
+ });
+ });
+
+ const handleVerifyDomain = () => {
+ if (!editingConfig) return;
+ verifyDomainMutation({
+ variables: {
+ input: {
+ id: editingConfig.id,
+ },
+ },
+ onCompleted: (response) => {
+ if (response.verifyDomain.verified) {
+ setCurrentStep("configure");
+ }
+ },
+ });
+ };
+
+ const onSubmit = form.handleSubmit((data) => {
+ if (editingConfig) {
+ updateMutation({
+ variables: {
+ input: {
+ id: editingConfig.id,
+ enforcementPolicy: data.enforcementPolicy,
+ idpEntityId: data.idpEntityId,
+ idpSsoUrl: data.idpSsoUrl,
+ idpCertificate: data.idpCertificate,
+ idpMetadataUrl: data.idpMetadataUrl || null,
+ attributeEmail: data.attributeEmail || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+ attributeFirstname: data.attributeFirstname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
+ attributeLastname: data.attributeLastname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
+ attributeRole: data.attributeRole || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
+ defaultRole: data.defaultRole || "MEMBER",
+ autoSignupEnabled: data.autoSignupEnabled || false,
+ },
+ },
+ onCompleted: () => {
+ handleCloseModal();
+ },
+ });
+ } else {
+ createMutation({
+ variables: {
+ input: {
+ organizationId: organization.id,
+ emailDomain: data.emailDomain,
+ enforcementPolicy: data.enforcementPolicy,
+ idpEntityId: data.idpEntityId,
+ idpSsoUrl: data.idpSsoUrl,
+ idpCertificate: data.idpCertificate,
+ idpMetadataUrl: data.idpMetadataUrl || null,
+ attributeEmail: data.attributeEmail || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+ attributeFirstname: data.attributeFirstname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
+ attributeLastname: data.attributeLastname || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
+ attributeRole: data.attributeRole || "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
+ defaultRole: data.defaultRole || "MEMBER",
+ autoSignupEnabled: data.autoSignupEnabled || false,
+ },
+ },
+ onCompleted: () => {
+ handleCloseModal();
+ },
+ });
+ }
+ });
+
+ const handleToggleEnabled = (config: typeof configs[0]) => {
+ if (config.enabled) {
+ confirm(
+ async () => {
+ disableMutation({
+ variables: {
+ input: {
+ id: config.id,
+ },
+ },
+ });
+ },
+ {
+ title: __("Disable SAML"),
+ message: __(
+ "Are you sure you want to disable SAML authentication for " + config.emailDomain + "?"
+ ),
+ label: __("Disable"),
+ variant: "danger",
+ }
+ );
+ } else {
+ enableMutation({
+ variables: {
+ input: {
+ id: config.id,
+ },
+ },
+ });
+ }
+ };
+
+ const handleDelete = (config: typeof configs[0]) => {
+ confirm(
+ async () => {
+ deleteMutation({
+ variables: {
+ input: {
+ id: config.id,
+ },
+ },
+ onCompleted: () => {
+ handleCloseModal();
+ },
+ });
+ },
+ {
+ title: __("Delete SAML Configuration"),
+ message: __(
+ "Are you sure you want to delete the SAML configuration for " + config.emailDomain + "? This action cannot be undone."
+ ),
+ label: __("Delete"),
+ variant: "danger",
+ }
+ );
+ };
+
+ const [copiedUrl, setCopiedUrl] = useState(null);
+
+ const handleCopy = (url: string) => {
+ navigator.clipboard.writeText(url);
+ setCopiedUrl(url);
+ setTimeout(() => setCopiedUrl(null), 2000);
+ };
+
+ const getEnforcementPolicyLabel = (policy: string) => {
+ switch (policy) {
+ case "OFF":
+ return __("Your team members can't use single sign-on and must use their password");
+ case "REQUIRED":
+ return __("Your team members must use single sign-on to log in");
+ case "OPTIONAL":
+ default:
+ return __("Your team members may use either single sign-on or their password to log in");
+ }
+ };
+
+ return (
+ <>
+
+
+
{__("SAML Single Sign-On")}
+ handleOpenModal()}>
+ {__("Add Configuration")}
+
+
+
+ {configs.length === 0 ? (
+
+
+
+ {__("No SAML Configurations")}
+
+
+ {__("Set up SAML 2.0 single sign-on for your organization by adding a configuration for each email domain.")}
+
+
handleOpenModal()}>
+ {__("Add Your First Configuration")}
+
+
+
+ ) : (
+
+
+
+ | {__("Email Domain")} |
+ {__("Domain Status")} |
+ {__("SAML Status")} |
+ {__("Enforcement")} |
+ {__("SSO URL")} |
+ |
+
+
+
+ {configs.map((config) => (
+
+ |
+ handleOpenModal(config)}
+ className="font-semibold text-blue-600 hover:text-blue-800"
+ >
+ {config.emailDomain}
+
+ |
+
+
+ {config.domainVerified ? __("Verified") : __("Pending Verification")}
+
+ |
+
+
+ {config.enabled ? __("Enabled") : __("Disabled")}
+
+ |
+ {config.enforcementPolicy} |
+
+ {config.domainVerified && config.enabled ? (
+ handleCopy(config.testLoginUrl)}
+ className="text-blue-600 hover:text-blue-800"
+ >
+ {copiedUrl === config.testLoginUrl ? __("Copied!") : __("Copy URL")}
+
+ ) : (
+ —
+ )}
+ |
+
+
+ {config.domainVerified ? (
+ <>
+ handleToggleEnabled(config)}
+ disabled={isEnabling || isDisabling}
+ >
+ {config.enabled ? __("Disable") : __("Enable")}
+
+ handleOpenModal(config)}
+ >
+ {__("Edit")}
+
+ >
+ ) : (
+ <>
+ handleOpenModal(config)}
+ >
+ {__("Verify Domain")}
+
+ handleDelete(config)}
+ >
+ {__("Delete")}
+
+ >
+ )}
+
+ |
+
+ ))}
+
+
+ )}
+
+
+
+ >
+ );
+}
diff --git a/apps/console/src/pages/organizations/settings/__generated__/DomainSettingsTabFragment.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/DomainSettingsTabFragment.graphql.ts
new file mode 100644
index 000000000..ed5d40158
--- /dev/null
+++ b/apps/console/src/pages/organizations/settings/__generated__/DomainSettingsTabFragment.graphql.ts
@@ -0,0 +1,154 @@
+/**
+ * @generated SignedSource<<0e2aa976b1c9bb8dddf8b1dcc2f171d1>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ReaderFragment } from 'relay-runtime';
+export type SSLStatus = "ACTIVE" | "EXPIRED" | "FAILED" | "PENDING" | "PROVISIONING" | "RENEWING";
+import { FragmentRefs } from "relay-runtime";
+export type DomainSettingsTabFragment$data = {
+ readonly customDomain: {
+ readonly createdAt: any;
+ readonly dnsRecords: ReadonlyArray<{
+ readonly name: string;
+ readonly purpose: string;
+ readonly ttl: number;
+ readonly type: string;
+ readonly value: string;
+ }>;
+ readonly domain: string;
+ readonly id: string;
+ readonly sslExpiresAt: any | null | undefined;
+ readonly sslStatus: SSLStatus;
+ readonly updatedAt: any;
+ } | null | undefined;
+ readonly id: string;
+ readonly " $fragmentType": "DomainSettingsTabFragment";
+};
+export type DomainSettingsTabFragment$key = {
+ readonly " $data"?: DomainSettingsTabFragment$data;
+ readonly " $fragmentSpreads": FragmentRefs<"DomainSettingsTabFragment">;
+};
+
+const node: ReaderFragment = (function(){
+var v0 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+};
+return {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "DomainSettingsTabFragment",
+ "selections": [
+ (v0/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "CustomDomain",
+ "kind": "LinkedField",
+ "name": "customDomain",
+ "plural": false,
+ "selections": [
+ (v0/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "domain",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "sslStatus",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "DNSRecordInstruction",
+ "kind": "LinkedField",
+ "name": "dnsRecords",
+ "plural": true,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "type",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "name",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "value",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "ttl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "purpose",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "updatedAt",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "sslExpiresAt",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "Organization",
+ "abstractKey": null
+};
+})();
+
+(node as any).hash = "00306efb96d302284155f5324ba2fb99";
+
+export default node;
diff --git a/apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTabFragment.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTabFragment.graphql.ts
new file mode 100644
index 000000000..4a2a592e4
--- /dev/null
+++ b/apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTabFragment.graphql.ts
@@ -0,0 +1,114 @@
+/**
+ * @generated SignedSource<<02beba57812b8dd7c5c61fb10291de5a>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ReaderFragment } from 'relay-runtime';
+import { FragmentRefs } from "relay-runtime";
+export type GeneralSettingsTabFragment$data = {
+ readonly createdAt: any;
+ readonly description: string | null | undefined;
+ readonly email: string | null | undefined;
+ readonly headquarterAddress: string | null | undefined;
+ readonly horizontalLogoUrl: string | null | undefined;
+ readonly id: string;
+ readonly logoUrl: string | null | undefined;
+ readonly name: string;
+ readonly updatedAt: any;
+ readonly websiteUrl: string | null | undefined;
+ readonly " $fragmentType": "GeneralSettingsTabFragment";
+};
+export type GeneralSettingsTabFragment$key = {
+ readonly " $data"?: GeneralSettingsTabFragment$data;
+ readonly " $fragmentSpreads": FragmentRefs<"GeneralSettingsTabFragment">;
+};
+
+const node: ReaderFragment = {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "GeneralSettingsTabFragment",
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "name",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "logoUrl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "horizontalLogoUrl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "description",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "websiteUrl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "email",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "headquarterAddress",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "updatedAt",
+ "storageKey": null
+ }
+ ],
+ "type": "Organization",
+ "abstractKey": null
+};
+
+(node as any).hash = "f6552148ce1c0061c4f5cbef78d46c0e";
+
+export default node;
diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteHorizontalLogoMutation.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTab_DeleteHorizontalLogoMutation.graphql.ts
similarity index 68%
rename from apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteHorizontalLogoMutation.graphql.ts
rename to apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTab_DeleteHorizontalLogoMutation.graphql.ts
index 3df9c7037..83f978a29 100644
--- a/apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteHorizontalLogoMutation.graphql.ts
+++ b/apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTab_DeleteHorizontalLogoMutation.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<<4ed3d530d746d8b84b2dba8752e57abc>>
+ * @generated SignedSource<<8e0818a9214ba3613f9d356e61a75e08>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -12,10 +12,10 @@ import { ConcreteRequest } from 'relay-runtime';
export type DeleteOrganizationHorizontalLogoInput = {
organizationId: string;
};
-export type SettingsPage_DeleteHorizontalLogoMutation$variables = {
+export type GeneralSettingsTab_DeleteHorizontalLogoMutation$variables = {
input: DeleteOrganizationHorizontalLogoInput;
};
-export type SettingsPage_DeleteHorizontalLogoMutation$data = {
+export type GeneralSettingsTab_DeleteHorizontalLogoMutation$data = {
readonly deleteOrganizationHorizontalLogo: {
readonly organization: {
readonly horizontalLogoUrl: string | null | undefined;
@@ -23,9 +23,9 @@ export type SettingsPage_DeleteHorizontalLogoMutation$data = {
};
};
};
-export type SettingsPage_DeleteHorizontalLogoMutation = {
- response: SettingsPage_DeleteHorizontalLogoMutation$data;
- variables: SettingsPage_DeleteHorizontalLogoMutation$variables;
+export type GeneralSettingsTab_DeleteHorizontalLogoMutation = {
+ response: GeneralSettingsTab_DeleteHorizontalLogoMutation$data;
+ variables: GeneralSettingsTab_DeleteHorizontalLogoMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -85,7 +85,7 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
- "name": "SettingsPage_DeleteHorizontalLogoMutation",
+ "name": "GeneralSettingsTab_DeleteHorizontalLogoMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
@@ -94,20 +94,20 @@ return {
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
- "name": "SettingsPage_DeleteHorizontalLogoMutation",
+ "name": "GeneralSettingsTab_DeleteHorizontalLogoMutation",
"selections": (v1/*: any*/)
},
"params": {
- "cacheID": "e631480d50c9347050fdc62075a2e3a3",
+ "cacheID": "fbfaf507b48e2ef274e44372a70b3d88",
"id": null,
"metadata": {},
- "name": "SettingsPage_DeleteHorizontalLogoMutation",
+ "name": "GeneralSettingsTab_DeleteHorizontalLogoMutation",
"operationKind": "mutation",
- "text": "mutation SettingsPage_DeleteHorizontalLogoMutation(\n $input: DeleteOrganizationHorizontalLogoInput!\n) {\n deleteOrganizationHorizontalLogo(input: $input) {\n organization {\n id\n horizontalLogoUrl\n }\n }\n}\n"
+ "text": "mutation GeneralSettingsTab_DeleteHorizontalLogoMutation(\n $input: DeleteOrganizationHorizontalLogoInput!\n) {\n deleteOrganizationHorizontalLogo(input: $input) {\n organization {\n id\n horizontalLogoUrl\n }\n }\n}\n"
}
};
})();
-(node as any).hash = "751c3ff44c59511451095ffc66446c2f";
+(node as any).hash = "7910936d423f99e36ee0a082f5c9336c";
export default node;
diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTab_UpdateMutation.graphql.ts
similarity index 80%
rename from apps/console/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts
rename to apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTab_UpdateMutation.graphql.ts
index bba467b7c..fc5690522 100644
--- a/apps/console/src/pages/organizations/__generated__/SettingsPage_UpdateMutation.graphql.ts
+++ b/apps/console/src/pages/organizations/settings/__generated__/GeneralSettingsTab_UpdateMutation.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<>
+ * @generated SignedSource<<270832e99647c6636914a9644a65358c>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -19,10 +19,10 @@ export type UpdateOrganizationInput = {
organizationId: string;
websiteUrl?: string | null | undefined;
};
-export type SettingsPage_UpdateMutation$variables = {
+export type GeneralSettingsTab_UpdateMutation$variables = {
input: UpdateOrganizationInput;
};
-export type SettingsPage_UpdateMutation$data = {
+export type GeneralSettingsTab_UpdateMutation$data = {
readonly updateOrganization: {
readonly organization: {
readonly description: string | null | undefined;
@@ -36,9 +36,9 @@ export type SettingsPage_UpdateMutation$data = {
};
};
};
-export type SettingsPage_UpdateMutation = {
- response: SettingsPage_UpdateMutation$data;
- variables: SettingsPage_UpdateMutation$variables;
+export type GeneralSettingsTab_UpdateMutation = {
+ response: GeneralSettingsTab_UpdateMutation$data;
+ variables: GeneralSettingsTab_UpdateMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -140,7 +140,7 @@ return {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
- "name": "SettingsPage_UpdateMutation",
+ "name": "GeneralSettingsTab_UpdateMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
@@ -149,20 +149,20 @@ return {
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
- "name": "SettingsPage_UpdateMutation",
+ "name": "GeneralSettingsTab_UpdateMutation",
"selections": (v1/*: any*/)
},
"params": {
- "cacheID": "097a6e519249d6f2b3c2a95963f85a6c",
+ "cacheID": "55c07b334317a5ca30023ef5354a6c45",
"id": null,
"metadata": {},
- "name": "SettingsPage_UpdateMutation",
+ "name": "GeneralSettingsTab_UpdateMutation",
"operationKind": "mutation",
- "text": "mutation SettingsPage_UpdateMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n }\n }\n}\n"
+ "text": "mutation GeneralSettingsTab_UpdateMutation(\n $input: UpdateOrganizationInput!\n) {\n updateOrganization(input: $input) {\n organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n }\n }\n}\n"
}
};
})();
-(node as any).hash = "c676129018636d84dbca9d8c98962af7";
+(node as any).hash = "f1731adcb4bf7b7214301a612f48567e";
export default node;
diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabInvitationsFragment.graphql.ts
similarity index 88%
rename from apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts
rename to apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabInvitationsFragment.graphql.ts
index 619841913..c2b17c5c6 100644
--- a/apps/console/src/pages/organizations/__generated__/SettingsPageInvitationsFragment.graphql.ts
+++ b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabInvitationsFragment.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<<7ca4537a89a599fe5d2b7ecad753aa64>>
+ * @generated SignedSource<<1077fac0cf9631664adf53347f21a5b0>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,7 +11,7 @@
import { ReaderFragment } from 'relay-runtime';
export type InvitationStatus = "ACCEPTED" | "EXPIRED" | "PENDING";
import { FragmentRefs } from "relay-runtime";
-export type SettingsPageInvitationsFragment$data = {
+export type MembersSettingsTabInvitationsFragment$data = {
readonly id: string;
readonly invitations: {
readonly __id: string;
@@ -29,14 +29,14 @@ export type SettingsPageInvitationsFragment$data = {
}>;
readonly totalCount: number;
};
- readonly " $fragmentType": "SettingsPageInvitationsFragment";
+ readonly " $fragmentType": "MembersSettingsTabInvitationsFragment";
};
-export type SettingsPageInvitationsFragment$key = {
- readonly " $data"?: SettingsPageInvitationsFragment$data;
- readonly " $fragmentSpreads": FragmentRefs<"SettingsPageInvitationsFragment">;
+export type MembersSettingsTabInvitationsFragment$key = {
+ readonly " $data"?: MembersSettingsTabInvitationsFragment$data;
+ readonly " $fragmentSpreads": FragmentRefs<"MembersSettingsTabInvitationsFragment">;
};
-import SettingsInvitationsRefetchQuery_graphql from './SettingsInvitationsRefetchQuery.graphql';
+import MembersSettingsTabInvitationsRefetchQuery_graphql from './MembersSettingsTabInvitationsRefetchQuery.graphql';
const node: ReaderFragment = (function(){
var v0 = [
@@ -105,28 +105,18 @@ return {
"fragmentPathInResult": [
"node"
],
- "operation": SettingsInvitationsRefetchQuery_graphql,
+ "operation": MembersSettingsTabInvitationsRefetchQuery_graphql,
"identifierInfo": {
"identifierField": "id",
"identifierQueryVariableName": "id"
}
}
},
- "name": "SettingsPageInvitationsFragment",
+ "name": "MembersSettingsTabInvitationsFragment",
"selections": [
{
"alias": "invitations",
"args": [
- {
- "kind": "Literal",
- "name": "filter",
- "value": {
- "statuses": [
- "PENDING",
- "EXPIRED"
- ]
- }
- },
{
"kind": "Variable",
"name": "orderBy",
@@ -135,7 +125,7 @@ return {
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
- "name": "__SettingsPageInvitations_invitations_connection",
+ "name": "__MembersSettingsTabInvitations_invitations_connection",
"plural": false,
"selections": [
{
@@ -166,14 +156,14 @@ return {
"alias": null,
"args": null,
"kind": "ScalarField",
- "name": "email",
+ "name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
- "name": "fullName",
+ "name": "email",
"storageKey": null
},
{
@@ -190,6 +180,13 @@ return {
"name": "status",
"storageKey": null
},
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ },
{
"alias": null,
"args": null,
@@ -204,13 +201,6 @@ return {
"name": "acceptedAt",
"storageKey": null
},
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "createdAt",
- "storageKey": null
- },
{
"alias": null,
"args": null,
@@ -292,6 +282,6 @@ return {
};
})();
-(node as any).hash = "b56157db731d3968bb825dd36375ddb7";
+(node as any).hash = "632fb80f7f536c576adaef2ec4007588";
export default node;
diff --git a/apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabInvitationsRefetchQuery.graphql.ts
similarity index 83%
rename from apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts
rename to apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabInvitationsRefetchQuery.graphql.ts
index bbd421fa1..aba5358fa 100644
--- a/apps/console/src/pages/organizations/__generated__/SettingsInvitationsRefetchQuery.graphql.ts
+++ b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabInvitationsRefetchQuery.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<<39e11ee9cf99dc541aa5a8df259e8438>>
+ * @generated SignedSource<<24e16bb5ea83a195376f3356801ecdd6>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -16,7 +16,7 @@ export type InvitationOrder = {
direction: OrderDirection;
field: InvitationOrderField;
};
-export type SettingsInvitationsRefetchQuery$variables = {
+export type MembersSettingsTabInvitationsRefetchQuery$variables = {
after?: any | null | undefined;
before?: any | null | undefined;
first?: number | null | undefined;
@@ -24,14 +24,14 @@ export type SettingsInvitationsRefetchQuery$variables = {
last?: number | null | undefined;
order?: InvitationOrder | null | undefined;
};
-export type SettingsInvitationsRefetchQuery$data = {
+export type MembersSettingsTabInvitationsRefetchQuery$data = {
readonly node: {
- readonly " $fragmentSpreads": FragmentRefs<"SettingsPageInvitationsFragment">;
+ readonly " $fragmentSpreads": FragmentRefs<"MembersSettingsTabInvitationsFragment">;
};
};
-export type SettingsInvitationsRefetchQuery = {
- response: SettingsInvitationsRefetchQuery$data;
- variables: SettingsInvitationsRefetchQuery$variables;
+export type MembersSettingsTabInvitationsRefetchQuery = {
+ response: MembersSettingsTabInvitationsRefetchQuery$data;
+ variables: MembersSettingsTabInvitationsRefetchQuery$variables;
};
const node: ConcreteRequest = (function(){
@@ -112,16 +112,6 @@ v12 = {
v13 = [
(v7/*: any*/),
(v8/*: any*/),
- {
- "kind": "Literal",
- "name": "filter",
- "value": {
- "statuses": [
- "PENDING",
- "EXPIRED"
- ]
- }
- },
(v9/*: any*/),
(v10/*: any*/),
{
@@ -142,7 +132,7 @@ return {
],
"kind": "Fragment",
"metadata": null,
- "name": "SettingsInvitationsRefetchQuery",
+ "name": "MembersSettingsTabInvitationsRefetchQuery",
"selections": [
{
"alias": null,
@@ -165,7 +155,7 @@ return {
}
],
"kind": "FragmentSpread",
- "name": "SettingsPageInvitationsFragment"
+ "name": "MembersSettingsTabInvitationsFragment"
}
],
"storageKey": null
@@ -185,7 +175,7 @@ return {
(v3/*: any*/)
],
"kind": "Operation",
- "name": "SettingsInvitationsRefetchQuery",
+ "name": "MembersSettingsTabInvitationsRefetchQuery",
"selections": [
{
"alias": null,
@@ -236,14 +226,14 @@ return {
"alias": null,
"args": null,
"kind": "ScalarField",
- "name": "email",
+ "name": "fullName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
- "name": "fullName",
+ "name": "email",
"storageKey": null
},
{
@@ -260,6 +250,13 @@ return {
"name": "status",
"storageKey": null
},
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "createdAt",
+ "storageKey": null
+ },
{
"alias": null,
"args": null,
@@ -274,13 +271,6 @@ return {
"name": "acceptedAt",
"storageKey": null
},
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "createdAt",
- "storageKey": null
- },
(v11/*: any*/)
],
"storageKey": null
@@ -353,11 +343,10 @@ return {
"alias": null,
"args": (v13/*: any*/),
"filters": [
- "orderBy",
- "filter"
+ "orderBy"
],
"handle": "connection",
- "key": "SettingsPageInvitations_invitations",
+ "key": "MembersSettingsTabInvitations_invitations",
"kind": "LinkedHandle",
"name": "invitations"
}
@@ -371,16 +360,16 @@ return {
]
},
"params": {
- "cacheID": "24b42a44e783a7f8499a568de2c8f9f4",
+ "cacheID": "e6105642c2d4bc7c1023e4456cebff8e",
"id": null,
"metadata": {},
- "name": "SettingsInvitationsRefetchQuery",
+ "name": "MembersSettingsTabInvitationsRefetchQuery",
"operationKind": "query",
- "text": "query SettingsInvitationsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: InvitationOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...SettingsPageInvitationsFragment_16fISc\n id\n }\n}\n\nfragment SettingsPageInvitationsFragment_16fISc on Organization {\n invitations(first: $first, after: $after, last: $last, before: $before, orderBy: $order, filter: {statuses: [PENDING, EXPIRED]}) {\n totalCount\n edges {\n node {\n id\n email\n fullName\n role\n status\n expiresAt\n acceptedAt\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
+ "text": "query MembersSettingsTabInvitationsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: InvitationOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MembersSettingsTabInvitationsFragment_16fISc\n id\n }\n}\n\nfragment MembersSettingsTabInvitationsFragment_16fISc on Organization {\n invitations(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n fullName\n email\n role\n status\n createdAt\n expiresAt\n acceptedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();
-(node as any).hash = "b56157db731d3968bb825dd36375ddb7";
+(node as any).hash = "632fb80f7f536c576adaef2ec4007588";
export default node;
diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPageMembershipsFragment.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabMembershipsFragment.graphql.ts
similarity index 84%
rename from apps/console/src/pages/organizations/__generated__/SettingsPageMembershipsFragment.graphql.ts
rename to apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabMembershipsFragment.graphql.ts
index 902b3977a..51921894e 100644
--- a/apps/console/src/pages/organizations/__generated__/SettingsPageMembershipsFragment.graphql.ts
+++ b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabMembershipsFragment.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<<778a6585d3f3d9b346273c20319ce96d>>
+ * @generated SignedSource<>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,13 +9,15 @@
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
+export type UserAuthMethod = "PASSWORD" | "SAML";
import { FragmentRefs } from "relay-runtime";
-export type SettingsPageMembershipsFragment$data = {
+export type MembersSettingsTabMembershipsFragment$data = {
readonly id: string;
readonly memberships: {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
+ readonly authMethod: UserAuthMethod;
readonly createdAt: any;
readonly emailAddress: string;
readonly fullName: string;
@@ -25,14 +27,14 @@ export type SettingsPageMembershipsFragment$data = {
}>;
readonly totalCount: number;
};
- readonly " $fragmentType": "SettingsPageMembershipsFragment";
+ readonly " $fragmentType": "MembersSettingsTabMembershipsFragment";
};
-export type SettingsPageMembershipsFragment$key = {
- readonly " $data"?: SettingsPageMembershipsFragment$data;
- readonly " $fragmentSpreads": FragmentRefs<"SettingsPageMembershipsFragment">;
+export type MembersSettingsTabMembershipsFragment$key = {
+ readonly " $data"?: MembersSettingsTabMembershipsFragment$data;
+ readonly " $fragmentSpreads": FragmentRefs<"MembersSettingsTabMembershipsFragment">;
};
-import SettingsMembershipsRefetchQuery_graphql from './SettingsMembershipsRefetchQuery.graphql';
+import MembersSettingsTabMembershipsRefetchQuery_graphql from './MembersSettingsTabMembershipsRefetchQuery.graphql';
const node: ReaderFragment = (function(){
var v0 = [
@@ -101,14 +103,14 @@ return {
"fragmentPathInResult": [
"node"
],
- "operation": SettingsMembershipsRefetchQuery_graphql,
+ "operation": MembersSettingsTabMembershipsRefetchQuery_graphql,
"identifierInfo": {
"identifierField": "id",
"identifierQueryVariableName": "id"
}
}
},
- "name": "SettingsPageMembershipsFragment",
+ "name": "MembersSettingsTabMembershipsFragment",
"selections": [
{
"alias": "memberships",
@@ -121,7 +123,7 @@ return {
],
"concreteType": "MembershipConnection",
"kind": "LinkedField",
- "name": "__SettingsPageMemberships_memberships_connection",
+ "name": "__MembersSettingsTabMemberships_memberships_connection",
"plural": false,
"selections": [
{
@@ -169,6 +171,13 @@ return {
"name": "role",
"storageKey": null
},
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "authMethod",
+ "storageKey": null
+ },
{
"alias": null,
"args": null,
@@ -257,6 +266,6 @@ return {
};
})();
-(node as any).hash = "d28514cdb5181fc1023dc4ea5bddb4f2";
+(node as any).hash = "c9e341e99052ba74299c5ddd0433d7c0";
export default node;
diff --git a/apps/console/src/pages/organizations/__generated__/SettingsMembershipsRefetchQuery.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabMembershipsRefetchQuery.graphql.ts
similarity index 81%
rename from apps/console/src/pages/organizations/__generated__/SettingsMembershipsRefetchQuery.graphql.ts
rename to apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabMembershipsRefetchQuery.graphql.ts
index 1cb1e791a..e6d8872b1 100644
--- a/apps/console/src/pages/organizations/__generated__/SettingsMembershipsRefetchQuery.graphql.ts
+++ b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTabMembershipsRefetchQuery.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<<49986e7498d757f2074e93e6cf4747a5>>
+ * @generated SignedSource<>
* @lightSyntaxTransform
* @nogrep
*/
@@ -16,7 +16,7 @@ export type MembershipOrder = {
direction: OrderDirection;
field: MembershipOrderField;
};
-export type SettingsMembershipsRefetchQuery$variables = {
+export type MembersSettingsTabMembershipsRefetchQuery$variables = {
after?: any | null | undefined;
before?: any | null | undefined;
first?: number | null | undefined;
@@ -24,14 +24,14 @@ export type SettingsMembershipsRefetchQuery$variables = {
last?: number | null | undefined;
order?: MembershipOrder | null | undefined;
};
-export type SettingsMembershipsRefetchQuery$data = {
+export type MembersSettingsTabMembershipsRefetchQuery$data = {
readonly node: {
- readonly " $fragmentSpreads": FragmentRefs<"SettingsPageMembershipsFragment">;
+ readonly " $fragmentSpreads": FragmentRefs<"MembersSettingsTabMembershipsFragment">;
};
};
-export type SettingsMembershipsRefetchQuery = {
- response: SettingsMembershipsRefetchQuery$data;
- variables: SettingsMembershipsRefetchQuery$variables;
+export type MembersSettingsTabMembershipsRefetchQuery = {
+ response: MembersSettingsTabMembershipsRefetchQuery$data;
+ variables: MembersSettingsTabMembershipsRefetchQuery$variables;
};
const node: ConcreteRequest = (function(){
@@ -132,7 +132,7 @@ return {
],
"kind": "Fragment",
"metadata": null,
- "name": "SettingsMembershipsRefetchQuery",
+ "name": "MembersSettingsTabMembershipsRefetchQuery",
"selections": [
{
"alias": null,
@@ -155,7 +155,7 @@ return {
}
],
"kind": "FragmentSpread",
- "name": "SettingsPageMembershipsFragment"
+ "name": "MembersSettingsTabMembershipsFragment"
}
],
"storageKey": null
@@ -175,7 +175,7 @@ return {
(v3/*: any*/)
],
"kind": "Operation",
- "name": "SettingsMembershipsRefetchQuery",
+ "name": "MembersSettingsTabMembershipsRefetchQuery",
"selections": [
{
"alias": null,
@@ -243,6 +243,13 @@ return {
"name": "role",
"storageKey": null
},
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "authMethod",
+ "storageKey": null
+ },
{
"alias": null,
"args": null,
@@ -325,7 +332,7 @@ return {
"orderBy"
],
"handle": "connection",
- "key": "SettingsPageMemberships_memberships",
+ "key": "MembersSettingsTabMemberships_memberships",
"kind": "LinkedHandle",
"name": "memberships"
}
@@ -339,16 +346,16 @@ return {
]
},
"params": {
- "cacheID": "57c4ca08006166b58c1fb2407f091704",
+ "cacheID": "27627ac3e1ea017ee7783fcfd502c0f8",
"id": null,
"metadata": {},
- "name": "SettingsMembershipsRefetchQuery",
+ "name": "MembersSettingsTabMembershipsRefetchQuery",
"operationKind": "query",
- "text": "query SettingsMembershipsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MembershipOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...SettingsPageMembershipsFragment_16fISc\n id\n }\n}\n\nfragment SettingsPageMembershipsFragment_16fISc on Organization {\n memberships(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
+ "text": "query MembersSettingsTabMembershipsRefetchQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 20\n $last: Int = null\n $order: MembershipOrder = {direction: ASC, field: CREATED_AT}\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...MembersSettingsTabMembershipsFragment_16fISc\n id\n }\n}\n\nfragment MembersSettingsTabMembershipsFragment_16fISc on Organization {\n memberships(first: $first, after: $after, last: $last, before: $before, orderBy: $order) {\n totalCount\n edges {\n node {\n id\n fullName\n emailAddress\n role\n authMethod\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();
-(node as any).hash = "d28514cdb5181fc1023dc4ea5bddb4f2";
+(node as any).hash = "c9e341e99052ba74299c5ddd0433d7c0";
export default node;
diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteInvitationMutation.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTab_DeleteInvitationMutation.graphql.ts
similarity index 73%
rename from apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteInvitationMutation.graphql.ts
rename to apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTab_DeleteInvitationMutation.graphql.ts
index d608f79ec..22822e45c 100644
--- a/apps/console/src/pages/organizations/__generated__/SettingsPage_DeleteInvitationMutation.graphql.ts
+++ b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTab_DeleteInvitationMutation.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<>
+ * @generated SignedSource<<057a86325a80ac7377b18a50896f01e1>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -12,18 +12,18 @@ import { ConcreteRequest } from 'relay-runtime';
export type DeleteInvitationInput = {
invitationId: string;
};
-export type SettingsPage_DeleteInvitationMutation$variables = {
+export type MembersSettingsTab_DeleteInvitationMutation$variables = {
connections: ReadonlyArray;
input: DeleteInvitationInput;
};
-export type SettingsPage_DeleteInvitationMutation$data = {
+export type MembersSettingsTab_DeleteInvitationMutation$data = {
readonly deleteInvitation: {
readonly deletedInvitationId: string;
};
};
-export type SettingsPage_DeleteInvitationMutation = {
- response: SettingsPage_DeleteInvitationMutation$data;
- variables: SettingsPage_DeleteInvitationMutation$variables;
+export type MembersSettingsTab_DeleteInvitationMutation = {
+ response: MembersSettingsTab_DeleteInvitationMutation$data;
+ variables: MembersSettingsTab_DeleteInvitationMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -59,7 +59,7 @@ return {
],
"kind": "Fragment",
"metadata": null,
- "name": "SettingsPage_DeleteInvitationMutation",
+ "name": "MembersSettingsTab_DeleteInvitationMutation",
"selections": [
{
"alias": null,
@@ -84,7 +84,7 @@ return {
(v0/*: any*/)
],
"kind": "Operation",
- "name": "SettingsPage_DeleteInvitationMutation",
+ "name": "MembersSettingsTab_DeleteInvitationMutation",
"selections": [
{
"alias": null,
@@ -117,16 +117,16 @@ return {
]
},
"params": {
- "cacheID": "1c362c5db7a985d7548166b2b1eb42c9",
+ "cacheID": "c995f13c967dab141f64f9ad6314f3a2",
"id": null,
"metadata": {},
- "name": "SettingsPage_DeleteInvitationMutation",
+ "name": "MembersSettingsTab_DeleteInvitationMutation",
"operationKind": "mutation",
- "text": "mutation SettingsPage_DeleteInvitationMutation(\n $input: DeleteInvitationInput!\n) {\n deleteInvitation(input: $input) {\n deletedInvitationId\n }\n}\n"
+ "text": "mutation MembersSettingsTab_DeleteInvitationMutation(\n $input: DeleteInvitationInput!\n) {\n deleteInvitation(input: $input) {\n deletedInvitationId\n }\n}\n"
}
};
})();
-(node as any).hash = "3c484508ba04b5a75eca62fa6afeb16d";
+(node as any).hash = "ad47509295919c7f0e6ff7895777231f";
export default node;
diff --git a/apps/console/src/pages/organizations/__generated__/SettingsPage_RemoveMemberMutation.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTab_RemoveMemberMutation.graphql.ts
similarity index 74%
rename from apps/console/src/pages/organizations/__generated__/SettingsPage_RemoveMemberMutation.graphql.ts
rename to apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTab_RemoveMemberMutation.graphql.ts
index cf363eea2..2dddd313d 100644
--- a/apps/console/src/pages/organizations/__generated__/SettingsPage_RemoveMemberMutation.graphql.ts
+++ b/apps/console/src/pages/organizations/settings/__generated__/MembersSettingsTab_RemoveMemberMutation.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<>
+ * @generated SignedSource<>
* @lightSyntaxTransform
* @nogrep
*/
@@ -13,18 +13,18 @@ export type RemoveMemberInput = {
memberId: string;
organizationId: string;
};
-export type SettingsPage_RemoveMemberMutation$variables = {
+export type MembersSettingsTab_RemoveMemberMutation$variables = {
connections: ReadonlyArray;
input: RemoveMemberInput;
};
-export type SettingsPage_RemoveMemberMutation$data = {
+export type MembersSettingsTab_RemoveMemberMutation$data = {
readonly removeMember: {
readonly deletedMemberId: string;
};
};
-export type SettingsPage_RemoveMemberMutation = {
- response: SettingsPage_RemoveMemberMutation$data;
- variables: SettingsPage_RemoveMemberMutation$variables;
+export type MembersSettingsTab_RemoveMemberMutation = {
+ response: MembersSettingsTab_RemoveMemberMutation$data;
+ variables: MembersSettingsTab_RemoveMemberMutation$variables;
};
const node: ConcreteRequest = (function(){
@@ -60,7 +60,7 @@ return {
],
"kind": "Fragment",
"metadata": null,
- "name": "SettingsPage_RemoveMemberMutation",
+ "name": "MembersSettingsTab_RemoveMemberMutation",
"selections": [
{
"alias": null,
@@ -85,7 +85,7 @@ return {
(v0/*: any*/)
],
"kind": "Operation",
- "name": "SettingsPage_RemoveMemberMutation",
+ "name": "MembersSettingsTab_RemoveMemberMutation",
"selections": [
{
"alias": null,
@@ -118,16 +118,16 @@ return {
]
},
"params": {
- "cacheID": "e2dd0f4d7327ce3bc97754c85d3f700d",
+ "cacheID": "6ccac45c6bedfbfe98b6c6344ea5df28",
"id": null,
"metadata": {},
- "name": "SettingsPage_RemoveMemberMutation",
+ "name": "MembersSettingsTab_RemoveMemberMutation",
"operationKind": "mutation",
- "text": "mutation SettingsPage_RemoveMemberMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n deletedMemberId\n }\n}\n"
+ "text": "mutation MembersSettingsTab_RemoveMemberMutation(\n $input: RemoveMemberInput!\n) {\n removeMember(input: $input) {\n deletedMemberId\n }\n}\n"
}
};
})();
-(node as any).hash = "9909a8b95f8d8621ffdf02da34ec8da2";
+(node as any).hash = "97f72349476066a0de4e580d2e4e1b0e";
export default node;
diff --git a/apps/console/src/pages/organizations/settings/__generated__/SAMLSettingsTabFragment.graphql.ts b/apps/console/src/pages/organizations/settings/__generated__/SAMLSettingsTabFragment.graphql.ts
new file mode 100644
index 000000000..3c0fa1503
--- /dev/null
+++ b/apps/console/src/pages/organizations/settings/__generated__/SAMLSettingsTabFragment.graphql.ts
@@ -0,0 +1,229 @@
+/**
+ * @generated SignedSource<<0b3ea1127a3a6388e6d0802abea0307c>>
+ * @lightSyntaxTransform
+ * @nogrep
+ */
+
+/* tslint:disable */
+/* eslint-disable */
+// @ts-nocheck
+
+import { ReaderFragment } from 'relay-runtime';
+export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED";
+import { FragmentRefs } from "relay-runtime";
+export type SAMLSettingsTabFragment$data = {
+ readonly id: string;
+ readonly name: string;
+ readonly samlConfigurations: ReadonlyArray<{
+ readonly attributeEmail: string;
+ readonly attributeFirstname: string;
+ readonly attributeLastname: string;
+ readonly attributeRole: string;
+ readonly autoSignupEnabled: boolean;
+ readonly defaultRole: string;
+ readonly domainVerificationToken: string | null | undefined;
+ readonly domainVerified: boolean;
+ readonly domainVerifiedAt: any | null | undefined;
+ readonly emailDomain: string;
+ readonly enabled: boolean;
+ readonly enforcementPolicy: SAMLEnforcementPolicy;
+ readonly id: string;
+ readonly idpCertificate: string;
+ readonly idpEntityId: string;
+ readonly idpMetadataUrl: string | null | undefined;
+ readonly idpSsoUrl: string;
+ readonly spAcsUrl: string;
+ readonly spEntityId: string;
+ readonly spMetadataUrl: string;
+ readonly testLoginUrl: string;
+ }>;
+ readonly " $fragmentType": "SAMLSettingsTabFragment";
+};
+export type SAMLSettingsTabFragment$key = {
+ readonly " $data"?: SAMLSettingsTabFragment$data;
+ readonly " $fragmentSpreads": FragmentRefs<"SAMLSettingsTabFragment">;
+};
+
+const node: ReaderFragment = (function(){
+var v0 = {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "id",
+ "storageKey": null
+};
+return {
+ "argumentDefinitions": [],
+ "kind": "Fragment",
+ "metadata": null,
+ "name": "SAMLSettingsTabFragment",
+ "selections": [
+ (v0/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "name",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "SAMLConfiguration",
+ "kind": "LinkedField",
+ "name": "samlConfigurations",
+ "plural": true,
+ "selections": [
+ (v0/*: any*/),
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "enabled",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "emailDomain",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "enforcementPolicy",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "domainVerified",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "domainVerificationToken",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "domainVerifiedAt",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "spEntityId",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "spAcsUrl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "spMetadataUrl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "testLoginUrl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "idpEntityId",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "idpSsoUrl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "idpCertificate",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "idpMetadataUrl",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "attributeEmail",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "attributeFirstname",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "attributeLastname",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "attributeRole",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "defaultRole",
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "autoSignupEnabled",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ }
+ ],
+ "type": "Organization",
+ "abstractKey": null
+};
+})();
+
+(node as any).hash = "691298e053f77bcb6ef13a5869a86579";
+
+export default node;
diff --git a/apps/console/src/providers/RelayProviders.tsx b/apps/console/src/providers/RelayProviders.tsx
index beb8caa2c..59a880fc3 100644
--- a/apps/console/src/providers/RelayProviders.tsx
+++ b/apps/console/src/providers/RelayProviders.tsx
@@ -23,6 +23,27 @@ export class InternalServerError extends Error {
}
}
+export class AuthenticationRequiredError extends Error {
+ public redirectUrl: string;
+ public requiresSaml: boolean;
+ public organizationId: string;
+ public samlConfigId?: string;
+
+ constructor(extensions: {
+ redirectUrl: string;
+ requiresSaml: boolean;
+ organizationId: string;
+ samlConfigId?: string;
+ }) {
+ super("AUTHENTICATION_REQUIRED");
+ this.name = "AuthenticationRequiredError";
+ this.redirectUrl = extensions.redirectUrl;
+ this.requiresSaml = extensions.requiresSaml;
+ this.organizationId = extensions.organizationId;
+ this.samlConfigId = extensions.samlConfigId;
+ }
+}
+
export function buildEndpoint(path: string): string {
const host = import.meta.env.VITE_API_URL;
@@ -47,6 +68,9 @@ export function buildEndpoint(path: string): string {
const hasUnauthenticatedError = (error: GraphQLError) =>
error.extensions?.code == "UNAUTHENTICATED";
+const hasAuthenticationRequiredError = (error: GraphQLError) =>
+ error.extensions?.code == "AUTHENTICATION_REQUIRED";
+
const fetchRelay: FetchFunction = async (
request,
variables,
@@ -117,6 +141,20 @@ const fetchRelay: FetchFunction = async (
throw new UnAuthenticatedError();
}
+ // Check for authentication required errors
+ const authRequiredError = errors.find(hasAuthenticationRequiredError);
+ if (authRequiredError?.extensions) {
+ const { redirectUrl, requiresSaml, organizationId, samlConfigId } = authRequiredError.extensions;
+
+ // Throw the error with all the redirect information
+ throw new AuthenticationRequiredError({
+ redirectUrl: redirectUrl as string,
+ requiresSaml: requiresSaml as boolean,
+ organizationId: organizationId as string,
+ samlConfigId: samlConfigId as string | undefined,
+ });
+ }
+
throw new Error(
`Error fetching GraphQL query '${
request.name
diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx
index 1a37806ee..a7b779e1b 100644
--- a/apps/console/src/routes.tsx
+++ b/apps/console/src/routes.tsx
@@ -50,7 +50,7 @@ function ErrorBoundary({ error: propsError }: { error?: string }) {
const error = useRouteError() ?? propsError;
if (error instanceof UnAuthenticatedError) {
- return ;
+ return ;
}
return ;
@@ -58,7 +58,7 @@ function ErrorBoundary({ error: propsError }: { error?: string }) {
const routes = [
{
- path: "/auth",
+ path: "/authentication",
Component: AuthLayout,
children: [
{
@@ -131,6 +131,30 @@ const routes = [
organizationId,
}),
Component: lazy(() => import("./pages/organizations/SettingsPage")),
+ children: [
+ {
+ path: "",
+ loader: () => {
+ throw redirect("general");
+ },
+ },
+ {
+ path: "general",
+ Component: lazy(() => import("./pages/organizations/settings/GeneralSettingsTab")),
+ },
+ {
+ path: "members",
+ Component: lazy(() => import("./pages/organizations/settings/MembersSettingsTab")),
+ },
+ {
+ path: "domain",
+ Component: lazy(() => import("./pages/organizations/settings/DomainSettingsTab")),
+ },
+ {
+ path: "saml-sso",
+ Component: lazy(() => import("./pages/organizations/settings/SAMLSettingsTab")),
+ },
+ ],
},
...riskRoutes,
...measureRoutes,
diff --git a/cfg/dev.yaml b/cfg/dev.yaml
index 132eaa2c2..fe5936f6a 100644
--- a/cfg/dev.yaml
+++ b/cfg/dev.yaml
@@ -9,7 +9,7 @@ unit:
max-queue-size: 2048
probod:
- hostname: "localhost:8080"
+ hostname: "https://gearnode.probo.engineering"
encryption-key: "thisisnotasecretAAAAAAAAAAAAAAAAAAAAAAAAAAA="
chrome-dp-addr: "localhost:9222"
diff --git a/go.mod b/go.mod
index 6eaaf53b4..557289d34 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0
github.com/chromedp/cdproto v0.0.0-20250630014756-b7288190f53c
github.com/chromedp/chromedp v0.13.7
+ github.com/crewjam/saml v0.5.1
github.com/go-chi/chi/v5 v5.2.2
github.com/go-chi/cors v1.2.2
github.com/jackc/pgx/v5 v5.7.5
@@ -45,6 +46,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect
github.com/aws/smithy-go v1.22.4 // indirect
+ github.com/beevik/etree v1.5.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
@@ -71,6 +73,8 @@ require (
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
+ github.com/jonboulle/clockwork v0.2.2 // indirect
+ github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/olekukonko/tablewriter v1.0.7 // indirect
@@ -82,6 +86,7 @@ require (
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/russellhaering/goxmldsig v1.4.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
diff --git a/go.sum b/go.sum
index 94eec51ba..bc4f4db8c 100644
--- a/go.sum
+++ b/go.sum
@@ -36,6 +36,9 @@ github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 h1:5Y75q0RPQoAbieyOuGLhjV9P3txvY
github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU=
github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw=
github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
+github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
+github.com/beevik/etree v1.5.0 h1:iaQZFSDS+3kYZiGoc9uKeOkUY3nYMXOKLl6KIJxiJWs=
+github.com/beevik/etree v1.5.0/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
@@ -52,6 +55,9 @@ github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipw
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/crewjam/saml v0.5.1 h1:g+mfp0CrLuLRZCK793PgJcZeg5dS/0CDwoeAX2zcwNI=
+github.com/crewjam/saml v0.5.1/go.mod h1:r0fDkmFe5URDgPrmtH0IYokva6fac3AUdstiPhyEolQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -80,6 +86,8 @@ github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
@@ -110,16 +118,25 @@ github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQykt
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw=
github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs=
+github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
+github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
+github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
+github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@@ -133,6 +150,7 @@ github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhA
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pdfcpu/pdfcpu v0.11.0 h1:mL18Y3hSHzSezmnrzA21TqlayBOXuAx7BUzzZyroLGM=
github.com/pdfcpu/pdfcpu v0.11.0/go.mod h1:F1ca4GIVFdPtmgvIdvXAycAm88noyNxZwzr9CpTy+Mw=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -153,8 +171,12 @@ github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTK
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/russellhaering/goxmldsig v1.4.0 h1:8UcDh/xGyQiyrW+Fq5t8f+l2DLB1+zlhYzkPUJ7Qhys=
+github.com/russellhaering/goxmldsig v1.4.0/go.mod h1:gM4MDENBQf7M+V824SGfyIUVFWydB7n0KkEubVJl+Tw=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
@@ -167,6 +189,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@@ -254,12 +277,17 @@ google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7E
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ=
sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4=
diff --git a/packages/ui/src/Layouts/AuthLayout.tsx b/packages/ui/src/Layouts/AuthLayout.tsx
index f0fb18382..930b200c3 100644
--- a/packages/ui/src/Layouts/AuthLayout.tsx
+++ b/packages/ui/src/Layouts/AuthLayout.tsx
@@ -5,7 +5,7 @@ export function AuthLayout() {
return (
-
diff --git a/pkg/auth/saml_cleanup.go b/pkg/auth/saml_cleanup.go
new file mode 100644
index 000000000..40bd742b0
--- /dev/null
+++ b/pkg/auth/saml_cleanup.go
@@ -0,0 +1,113 @@
+// Copyright (c) 2025 Probo Inc
.
+//
+// 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 auth
+
+import (
+ "context"
+ "time"
+
+ "go.gearno.de/kit/log"
+ "go.gearno.de/kit/pg"
+)
+
+const (
+ DefaultCleanupInterval = 1 * time.Hour
+)
+
+type (
+ Cleaner struct {
+ pg *pg.Client
+ interval time.Duration
+ logger *log.Logger
+ }
+)
+
+func NewCleaner(
+ pg *pg.Client,
+ interval time.Duration,
+ logger *log.Logger,
+) *Cleaner {
+ if interval == 0 {
+ interval = DefaultCleanupInterval
+ }
+
+ return &Cleaner{
+ pg: pg,
+ interval: interval,
+ logger: logger.Named("saml.cleaner"),
+ }
+}
+
+func (c *Cleaner) Run(ctx context.Context) error {
+ c.logger.InfoCtx(ctx, "SAML cleaner starting", log.Duration("interval", c.interval))
+
+ if err := c.cleanup(ctx); err != nil {
+ c.logger.ErrorCtx(ctx, "initial cleanup failed", log.Error(err))
+ }
+
+ ticker := time.NewTicker(c.interval)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ c.logger.InfoCtx(ctx, "SAML cleaner shutting down")
+ return ctx.Err()
+ case <-ticker.C:
+ if err := c.cleanup(ctx); err != nil {
+ c.logger.ErrorCtx(ctx, "periodic cleanup failed", log.Error(err))
+ }
+ }
+ }
+}
+
+func (c *Cleaner) cleanup(ctx context.Context) error {
+ var assertionsDeleted, requestsDeleted, relayStatesDeleted int64
+
+ err := c.pg.WithConn(ctx, func(conn pg.Conn) error {
+ count, err := CleanupExpiredAssertions(ctx, conn)
+ if err != nil {
+ return err
+ }
+ assertionsDeleted = count
+
+ count, err = CleanupExpiredRequests(ctx, conn)
+ if err != nil {
+ return err
+ }
+ requestsDeleted = count
+
+ count, err = CleanupExpiredRelayStates(ctx, conn)
+ if err != nil {
+ return err
+ }
+ relayStatesDeleted = count
+
+ return nil
+ })
+
+ if err != nil {
+ return err
+ }
+
+ if assertionsDeleted > 0 || requestsDeleted > 0 || relayStatesDeleted > 0 {
+ c.logger.InfoCtx(ctx, "cleaned up expired SAML data",
+ log.Int64("assertions", assertionsDeleted),
+ log.Int64("requests", requestsDeleted),
+ log.Int64("relay_states", relayStatesDeleted))
+ }
+
+ return nil
+}
diff --git a/pkg/auth/saml_config_validator.go b/pkg/auth/saml_config_validator.go
new file mode 100644
index 000000000..55e9e2c75
--- /dev/null
+++ b/pkg/auth/saml_config_validator.go
@@ -0,0 +1,127 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "crypto/x509"
+ "encoding/pem"
+ "fmt"
+ "net/url"
+)
+
+type ValidationError struct {
+ Field string
+ Message string
+}
+
+func (e ValidationError) Error() string {
+ return fmt.Sprintf("%s: %s", e.Field, e.Message)
+}
+
+// ValidateIdPConfiguration validates only the IdP (Identity Provider) configuration.
+// This validates user-provided data from the IdP.
+// SP (Service Provider) configuration is generated by the application and doesn't need validation.
+func ValidateIdPConfiguration(
+ idpEntityID string,
+ idpSsoURL string,
+ idpCertificate string,
+) []ValidationError {
+ var errors []ValidationError
+
+ // Validate IdP Entity ID
+ if idpEntityID == "" {
+ errors = append(errors, ValidationError{
+ Field: "idp_entity_id",
+ Message: "IdP Entity ID cannot be empty",
+ })
+ }
+
+ // Validate IdP SSO URL - accept both HTTP and HTTPS
+ if err := validateURL(idpSsoURL, "idp_sso_url"); err != nil {
+ errors = append(errors, *err)
+ }
+
+ // Validate IdP certificate
+ if err := validateCertificate(idpCertificate); err != nil {
+ errors = append(errors, ValidationError{
+ Field: "idp_certificate",
+ Message: err.Error(),
+ })
+ }
+
+ return errors
+}
+
+func validateURL(urlStr string, fieldName string) *ValidationError {
+ if urlStr == "" {
+ return &ValidationError{
+ Field: fieldName,
+ Message: "URL cannot be empty",
+ }
+ }
+
+ parsedURL, err := url.Parse(urlStr)
+ if err != nil {
+ return &ValidationError{
+ Field: fieldName,
+ Message: fmt.Sprintf("invalid URL format: %v", err),
+ }
+ }
+
+ if parsedURL.Scheme == "" {
+ return &ValidationError{
+ Field: fieldName,
+ Message: "URL must have a scheme (http or https)",
+ }
+ }
+
+ if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" {
+ return &ValidationError{
+ Field: fieldName,
+ Message: "URL scheme must be http or https (found: " + parsedURL.Scheme + ")",
+ }
+ }
+
+ if parsedURL.Host == "" {
+ return &ValidationError{
+ Field: fieldName,
+ Message: "URL must have a host",
+ }
+ }
+
+ return nil
+}
+
+func validateCertificate(certPEM string) error {
+ if certPEM == "" {
+ return fmt.Errorf("certificate cannot be empty")
+ }
+
+ block, _ := pem.Decode([]byte(certPEM))
+ if block == nil {
+ return fmt.Errorf("failed to parse certificate PEM")
+ }
+
+ if block.Type != "CERTIFICATE" {
+ return fmt.Errorf("PEM block type must be CERTIFICATE (found: %s)", block.Type)
+ }
+
+ _, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return fmt.Errorf("failed to parse X.509 certificate: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/auth/saml_configuration_service.go b/pkg/auth/saml_configuration_service.go
new file mode 100644
index 000000000..5a7092f59
--- /dev/null
+++ b/pkg/auth/saml_configuration_service.go
@@ -0,0 +1,304 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/getprobo/probo/pkg/coredata"
+ "github.com/getprobo/probo/pkg/gid"
+ "go.gearno.de/kit/pg"
+)
+
+type (
+ CreateSAMLConfigurationRequest struct {
+ OrganizationID gid.GID
+ EmailDomain string
+ EnforcementPolicy coredata.SAMLEnforcementPolicy
+ IdPEntityID string
+ IdPSsoURL string
+ IdPCertificate string
+ IdPMetadataURL *string
+ AttributeEmail string
+ AttributeFirstname string
+ AttributeLastname string
+ AttributeRole string
+ DefaultRole string
+ AutoSignupEnabled bool
+ }
+
+ UpdateSAMLConfigurationRequest struct {
+ ID gid.GID
+ Enabled *bool
+ EnforcementPolicy *coredata.SAMLEnforcementPolicy
+ IdPEntityID *string
+ IdPSsoURL *string
+ IdPCertificate *string
+ IdPMetadataURL *string
+ AttributeEmail *string
+ AttributeFirstname *string
+ AttributeLastname *string
+ AttributeRole *string
+ DefaultRole *string
+ AutoSignupEnabled *bool
+ }
+)
+
+func (s TenantAuthService) CreateSAMLConfiguration(
+ ctx context.Context,
+ req CreateSAMLConfigurationRequest,
+) (*coredata.SAMLConfiguration, error) {
+ // Validate only the IdP configuration (user-provided data)
+ validationErrors := ValidateIdPConfiguration(
+ req.IdPEntityID,
+ req.IdPSsoURL,
+ req.IdPCertificate,
+ )
+
+ if len(validationErrors) > 0 {
+ var errMsgs []string
+ for _, err := range validationErrors {
+ errMsgs = append(errMsgs, err.Error())
+ }
+ return nil, fmt.Errorf("SAML configuration validation failed: %s", strings.Join(errMsgs, "; "))
+ }
+
+ var config *coredata.SAMLConfiguration
+
+ err := s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ now := time.Now()
+ tenantID := s.scope.GetTenantID()
+
+ var org coredata.Organization
+ if err := org.LoadByID(ctx, tx, s.scope, req.OrganizationID); err != nil {
+ return fmt.Errorf("organization not found: %w", err)
+ }
+
+ config = &coredata.SAMLConfiguration{
+ ID: gid.New(tenantID, coredata.SAMLConfigurationEntityType),
+ OrganizationID: org.ID,
+ EmailDomain: req.EmailDomain,
+ EnforcementPolicy: req.EnforcementPolicy,
+ Enabled: false,
+ IdPEntityID: req.IdPEntityID,
+ IdPSsoURL: req.IdPSsoURL,
+ IdPCertificate: req.IdPCertificate,
+ IdPMetadataURL: req.IdPMetadataURL,
+ AttributeEmail: req.AttributeEmail,
+ AttributeFirstname: req.AttributeFirstname,
+ AttributeLastname: req.AttributeLastname,
+ AttributeRole: req.AttributeRole,
+ DefaultRole: req.DefaultRole,
+ AutoSignupEnabled: req.AutoSignupEnabled,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ if err := config.Insert(ctx, tx, s.scope); err != nil {
+ return fmt.Errorf("cannot insert saml configuration: %w", err)
+ }
+
+ return nil
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return config, nil
+}
+
+func (s TenantAuthService) UpdateSAMLConfiguration(
+ ctx context.Context,
+ req UpdateSAMLConfigurationRequest,
+) (*coredata.SAMLConfiguration, error) {
+ var config *coredata.SAMLConfiguration
+
+ err := s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ var cfg coredata.SAMLConfiguration
+ if err := cfg.LoadByID(ctx, tx, s.scope, req.ID); err != nil {
+ return fmt.Errorf("cannot load saml configuration: %w", err)
+ }
+
+ if req.Enabled != nil {
+ cfg.Enabled = *req.Enabled
+ }
+ if req.EnforcementPolicy != nil {
+ cfg.EnforcementPolicy = *req.EnforcementPolicy
+ }
+ if req.IdPEntityID != nil {
+ cfg.IdPEntityID = *req.IdPEntityID
+ }
+ if req.IdPSsoURL != nil {
+ cfg.IdPSsoURL = *req.IdPSsoURL
+ }
+ if req.IdPCertificate != nil {
+ cfg.IdPCertificate = *req.IdPCertificate
+ }
+ if req.IdPMetadataURL != nil {
+ cfg.IdPMetadataURL = req.IdPMetadataURL
+ }
+ if req.AttributeEmail != nil {
+ cfg.AttributeEmail = *req.AttributeEmail
+ }
+ if req.AttributeFirstname != nil {
+ cfg.AttributeFirstname = *req.AttributeFirstname
+ }
+ if req.AttributeLastname != nil {
+ cfg.AttributeLastname = *req.AttributeLastname
+ }
+ if req.AttributeRole != nil {
+ cfg.AttributeRole = *req.AttributeRole
+ }
+ if req.DefaultRole != nil {
+ cfg.DefaultRole = *req.DefaultRole
+ }
+ if req.AutoSignupEnabled != nil {
+ cfg.AutoSignupEnabled = *req.AutoSignupEnabled
+ }
+
+ cfg.UpdatedAt = time.Now()
+
+ if err := cfg.Update(ctx, tx, s.scope); err != nil {
+ return fmt.Errorf("cannot update saml configuration: %w", err)
+ }
+
+ config = &cfg
+ return nil
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ return config, nil
+}
+
+func (s TenantAuthService) DeleteSAMLConfiguration(
+ ctx context.Context,
+ configID gid.GID,
+) error {
+ return s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ var config coredata.SAMLConfiguration
+ if err := config.LoadByID(ctx, tx, s.scope, configID); err != nil {
+ return fmt.Errorf("cannot load saml configuration: %w", err)
+ }
+
+ if err := config.Delete(ctx, tx, s.scope); err != nil {
+ return fmt.Errorf("cannot delete saml configuration: %w", err)
+ }
+
+ return nil
+ },
+ )
+}
+
+func (s TenantAuthService) EnableSAMLConfiguration(
+ ctx context.Context,
+ configID gid.GID,
+) (*coredata.SAMLConfiguration, error) {
+ enabled := true
+ return s.UpdateSAMLConfiguration(ctx, UpdateSAMLConfigurationRequest{
+ ID: configID,
+ Enabled: &enabled,
+ })
+}
+
+func (s TenantAuthService) DisableSAMLConfiguration(
+ ctx context.Context,
+ configID gid.GID,
+) (*coredata.SAMLConfiguration, error) {
+ disabled := false
+ return s.UpdateSAMLConfiguration(ctx, UpdateSAMLConfigurationRequest{
+ ID: configID,
+ Enabled: &disabled,
+ })
+}
+
+func (s TenantAuthService) GetSAMLConfigurationByID(
+ ctx context.Context,
+ configID gid.GID,
+) (*coredata.SAMLConfiguration, error) {
+ var config coredata.SAMLConfiguration
+
+ err := s.pg.WithConn(
+ ctx,
+ func(conn pg.Conn) error {
+ return config.LoadByID(ctx, conn, s.scope, configID)
+ },
+ )
+ if err != nil {
+ return nil, fmt.Errorf("cannot load saml configuration: %w", err)
+ }
+
+ return &config, nil
+}
+
+func (s TenantAuthService) GetSAMLConfigurationsByOrganizationID(
+ ctx context.Context,
+ organizationID gid.GID,
+) ([]*coredata.SAMLConfiguration, error) {
+ var configs []*coredata.SAMLConfiguration
+
+ err := s.pg.WithConn(
+ ctx,
+ func(conn pg.Conn) error {
+ var err error
+ configs, err = coredata.LoadSAMLConfigurationsByOrganizationID(ctx, conn, s.scope, organizationID)
+ return err
+ },
+ )
+ if err != nil {
+ return nil, fmt.Errorf("cannot load saml configurations: %w", err)
+ }
+
+ return configs, nil
+}
+
+func (s Service) CheckSSOAvailabilityByEmail(
+ ctx context.Context,
+ email string,
+) ([]*coredata.SAMLConfiguration, error) {
+ // Extract domain from email
+ parts := strings.Split(email, "@")
+ if len(parts) != 2 {
+ return nil, fmt.Errorf("invalid email format")
+ }
+ domain := parts[1]
+
+ var configs []*coredata.SAMLConfiguration
+ err := s.pg.WithConn(
+ ctx,
+ func(conn pg.Conn) error {
+ var err error
+ configs, err = coredata.LoadAllEnabledSAMLConfigurationsByEmailDomain(ctx, conn, domain)
+ return err
+ },
+ )
+ if err != nil {
+ return nil, fmt.Errorf("cannot load saml configurations: %w", err)
+ }
+
+ return configs, nil
+}
diff --git a/pkg/auth/saml_mapper.go b/pkg/auth/saml_mapper.go
new file mode 100644
index 000000000..738b0ed72
--- /dev/null
+++ b/pkg/auth/saml_mapper.go
@@ -0,0 +1,147 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/crewjam/saml"
+)
+
+func ExtractAttributeValue(assertion *saml.Assertion, attributeName string) (string, error) {
+ if len(assertion.AttributeStatements) == 0 {
+ return "", fmt.Errorf("no attribute statement in assertion")
+ }
+
+ for _, attr := range assertion.AttributeStatements[0].Attributes {
+ if attr.Name == attributeName {
+ if len(attr.Values) == 0 {
+ return "", fmt.Errorf("attribute %q has no values", attributeName)
+ }
+ return attr.Values[0].Value, nil
+ }
+ }
+
+ return "", fmt.Errorf("attribute %q not found in assertion", attributeName)
+}
+
+func ExtractEmailFromAssertion(assertion *saml.Assertion) (string, error) {
+ commonEmailAttributes := []string{
+ "email",
+ "Email",
+ "emailAddress",
+ "mail",
+ "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+ "http://schemas.xmlsoap.org/claims/EmailAddress",
+ }
+
+ for _, attrName := range commonEmailAttributes {
+ email, err := ExtractAttributeValue(assertion, attrName)
+ if err == nil && email != "" {
+ return email, nil
+ }
+ }
+
+ if assertion.Subject != nil && assertion.Subject.NameID != nil && assertion.Subject.NameID.Value != "" {
+ return assertion.Subject.NameID.Value, nil
+ }
+
+ return "", fmt.Errorf("could not extract email from assertion")
+}
+
+func ExtractEmailDomain(email string) (string, error) {
+ parts := strings.Split(email, "@")
+ if len(parts) != 2 {
+ return "", fmt.Errorf("invalid email address: %s", email)
+ }
+ domain := strings.ToLower(strings.TrimSpace(parts[1]))
+ if domain == "" {
+ return "", fmt.Errorf("empty domain in email address: %s", email)
+ }
+ return domain, nil
+}
+
+func MapSAMLRoleToSystemRole(samlRole string, defaultRole string) (string, error) {
+ if samlRole != "" && isValidRole(samlRole) {
+ return samlRole, nil
+ }
+
+ if !isValidRole(defaultRole) {
+ return "", fmt.Errorf("invalid default role %q", defaultRole)
+ }
+
+ return defaultRole, nil
+}
+
+func isValidRole(role string) bool {
+ switch role {
+ case "OWNER", "ADMIN", "MEMBER", "VIEWER":
+ return true
+ default:
+ return false
+ }
+}
+
+func ExtractUserAttributes(
+ assertion *saml.Assertion,
+ attributeEmail, attributeFirstname, attributeLastname, attributeRole string,
+) (email, fullname, role string, err error) {
+ if len(assertion.AttributeStatements) == 0 {
+ if assertion.Subject != nil && assertion.Subject.NameID != nil {
+ email = assertion.Subject.NameID.Value
+ fullname = email
+ role = ""
+ return email, fullname, role, nil
+ }
+ return "", "", "", fmt.Errorf("no attribute statement and no NameID in assertion")
+ }
+
+ email, err = ExtractAttributeValue(assertion, attributeEmail)
+ if err != nil {
+ if assertion.Subject != nil && assertion.Subject.NameID != nil {
+ email = assertion.Subject.NameID.Value
+ } else {
+ return "", "", "", fmt.Errorf("failed to extract email: %w", err)
+ }
+ }
+
+ firstname, err := ExtractAttributeValue(assertion, attributeFirstname)
+ if err != nil {
+ firstname = ""
+ }
+
+ lastname, err := ExtractAttributeValue(assertion, attributeLastname)
+ if err != nil {
+ lastname = ""
+ }
+
+ if firstname != "" && lastname != "" {
+ fullname = strings.TrimSpace(firstname + " " + lastname)
+ } else if firstname != "" {
+ fullname = firstname
+ } else if lastname != "" {
+ fullname = lastname
+ } else {
+ fullname = email
+ }
+
+ role, err = ExtractAttributeValue(assertion, attributeRole)
+ if err != nil {
+ role = ""
+ }
+
+ return email, fullname, role, nil
+}
diff --git a/pkg/auth/saml_metadata.go b/pkg/auth/saml_metadata.go
new file mode 100644
index 000000000..d0da6eb68
--- /dev/null
+++ b/pkg/auth/saml_metadata.go
@@ -0,0 +1,204 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/base64"
+ "encoding/pem"
+ "encoding/xml"
+ "fmt"
+ "math/big"
+ "time"
+
+ "github.com/crewjam/saml"
+)
+
+func GenerateServiceProviderMetadata(
+ entityID string,
+ acsURL string,
+ spCert *x509.Certificate,
+) ([]byte, error) {
+ certData := base64.StdEncoding.EncodeToString(spCert.Raw)
+
+ trueVal := true
+
+ metadata := &saml.EntityDescriptor{
+ EntityID: entityID,
+ SPSSODescriptors: []saml.SPSSODescriptor{
+ {
+ SSODescriptor: saml.SSODescriptor{
+ RoleDescriptor: saml.RoleDescriptor{
+ ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
+ KeyDescriptors: []saml.KeyDescriptor{
+ {
+ Use: "signing",
+ KeyInfo: saml.KeyInfo{
+ X509Data: saml.X509Data{
+ X509Certificates: []saml.X509Certificate{
+ {Data: certData},
+ },
+ },
+ },
+ },
+ {
+ Use: "encryption",
+ KeyInfo: saml.KeyInfo{
+ X509Data: saml.X509Data{
+ X509Certificates: []saml.X509Certificate{
+ {Data: certData},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ AuthnRequestsSigned: &trueVal,
+ WantAssertionsSigned: &trueVal,
+ AssertionConsumerServices: []saml.IndexedEndpoint{
+ {
+ Binding: saml.HTTPPostBinding,
+ Location: acsURL,
+ Index: 0,
+ },
+ },
+ },
+ },
+ }
+
+ xmlBytes, err := xml.MarshalIndent(metadata, "", " ")
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal SP metadata to XML: %w", err)
+ }
+
+ return xmlBytes, nil
+}
+
+func ParseIdPCertificate(certPEM string) (*x509.Certificate, error) {
+ block, _ := pem.Decode([]byte(certPEM))
+ if block == nil {
+ return nil, fmt.Errorf("failed to decode PEM block from IdP certificate")
+ }
+
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse X.509 certificate: %w", err)
+ }
+
+ return cert, nil
+}
+
+type IdPMetadata struct {
+ EntityID string
+ SsoURL string
+ Certificate string
+ MetadataURL *string
+}
+
+func ParseIdPMetadata(metadataXML string) (*IdPMetadata, error) {
+ var entityDescriptor saml.EntityDescriptor
+ if err := xml.Unmarshal([]byte(metadataXML), &entityDescriptor); err != nil {
+ return nil, fmt.Errorf("failed to parse IdP metadata XML: %w", err)
+ }
+
+ if len(entityDescriptor.IDPSSODescriptors) == 0 {
+ return nil, fmt.Errorf("no IDPSSODescriptor found in metadata")
+ }
+
+ idpDescriptor := entityDescriptor.IDPSSODescriptors[0]
+
+ var ssoURL string
+ for _, sso := range idpDescriptor.SingleSignOnServices {
+ if sso.Binding == saml.HTTPPostBinding || sso.Binding == saml.HTTPRedirectBinding {
+ ssoURL = sso.Location
+ break
+ }
+ }
+ if ssoURL == "" && len(idpDescriptor.SingleSignOnServices) > 0 {
+ ssoURL = idpDescriptor.SingleSignOnServices[0].Location
+ }
+ if ssoURL == "" {
+ return nil, fmt.Errorf("no SingleSignOnService found in metadata")
+ }
+
+ var certPEM string
+ for _, keyDescriptor := range idpDescriptor.KeyDescriptors {
+ if keyDescriptor.Use == "signing" || keyDescriptor.Use == "" {
+ if len(keyDescriptor.KeyInfo.X509Data.X509Certificates) > 0 {
+ certData := keyDescriptor.KeyInfo.X509Data.X509Certificates[0].Data
+ certDER, err := base64.StdEncoding.DecodeString(certData)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode certificate: %w", err)
+ }
+ certPEM = string(pem.EncodeToMemory(&pem.Block{
+ Type: "CERTIFICATE",
+ Bytes: certDER,
+ }))
+ break
+ }
+ }
+ }
+ if certPEM == "" {
+ return nil, fmt.Errorf("no signing certificate found in metadata")
+ }
+
+ return &IdPMetadata{
+ EntityID: entityDescriptor.EntityID,
+ SsoURL: ssoURL,
+ Certificate: certPEM,
+ }, nil
+}
+
+func GenerateSelfSignedCertificate(entityID string) (*x509.Certificate, *rsa.PrivateKey, error) {
+ privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to generate RSA private key: %w", err)
+ }
+
+ serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+ serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to generate serial number: %w", err)
+ }
+
+ template := x509.Certificate{
+ SerialNumber: serialNumber,
+ Subject: pkix.Name{
+ CommonName: entityID,
+ Organization: []string{"Probo"},
+ },
+ NotBefore: time.Now(),
+ NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
+ KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
+ ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
+ BasicConstraintsValid: true,
+ }
+
+ certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to create certificate: %w", err)
+ }
+
+ cert, err := x509.ParseCertificate(certDER)
+ if err != nil {
+ return nil, nil, fmt.Errorf("failed to parse created certificate: %w", err)
+ }
+
+ return cert, privateKey, nil
+}
diff --git a/pkg/auth/saml_service.go b/pkg/auth/saml_service.go
new file mode 100644
index 000000000..863aff2fb
--- /dev/null
+++ b/pkg/auth/saml_service.go
@@ -0,0 +1,585 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "context"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/base64"
+ "encoding/pem"
+ "encoding/xml"
+ "fmt"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/crewjam/saml"
+ "github.com/getprobo/probo/pkg/coredata"
+ "github.com/getprobo/probo/pkg/crypto/cipher"
+ "github.com/getprobo/probo/pkg/gid"
+ "go.gearno.de/kit/log"
+ "go.gearno.de/kit/pg"
+)
+
+type (
+ SAMLService struct {
+ pg *pg.Client
+ encryptionKey cipher.EncryptionKey
+ baseURL string
+ sessionDuration time.Duration
+ cookieName string
+ cookieSecret string
+ certificate *x509.Certificate
+ privateKey *rsa.PrivateKey
+ logger *log.Logger
+ }
+
+ ErrSPCertificateNotConfigured struct{}
+
+ ErrSAMLConfigurationNotFound struct {
+ OrganizationID gid.GID
+ }
+
+ ErrSAMLDisabled struct {
+ OrganizationID gid.GID
+ }
+
+ ErrInvalidIdPCertificate struct {
+ Err error
+ }
+
+ ErrInvalidURL struct {
+ Field string
+ URL string
+ Err error
+ }
+
+ ErrCannotCreateServiceProvider struct {
+ Err error
+ }
+
+ ErrCannotCreateAuthRequest struct {
+ Err error
+ }
+
+ ErrCannotGenerateRedirectURL struct {
+ Err error
+ }
+
+ ErrCannotParseSAMLResponse struct {
+ Err error
+ }
+
+ ErrCannotValidateAssertion struct {
+ Err error
+ }
+
+ ErrCannotExtractUserAttributes struct {
+ Err error
+ }
+
+ ErrCannotMapRole struct {
+ Err error
+ }
+
+ ErrReplayAttackDetected struct {
+ AssertionID string
+ Err error
+ }
+)
+
+func (e ErrSPCertificateNotConfigured) Error() string {
+ return "SP certificate and private key are not configured"
+}
+
+func (e ErrSAMLConfigurationNotFound) Error() string {
+ return fmt.Sprintf("SAML configuration not found for organization %s", e.OrganizationID)
+}
+
+func (e ErrSAMLDisabled) Error() string {
+ return fmt.Sprintf("SAML is disabled for organization %s", e.OrganizationID)
+}
+
+func (e ErrInvalidIdPCertificate) Error() string {
+ return fmt.Sprintf("cannot parse IdP certificate: %v", e.Err)
+}
+
+func (e ErrInvalidURL) Error() string {
+ return fmt.Sprintf("cannot parse %s URL %q: %v", e.Field, e.URL, e.Err)
+}
+
+func (e ErrCannotCreateServiceProvider) Error() string {
+ return fmt.Sprintf("cannot create service provider: %v", e.Err)
+}
+
+func (e ErrCannotCreateAuthRequest) Error() string {
+ return fmt.Sprintf("cannot create AuthnRequest: %v", e.Err)
+}
+
+func (e ErrCannotGenerateRedirectURL) Error() string {
+ return fmt.Sprintf("cannot generate redirect URL: %v", e.Err)
+}
+
+func (e ErrCannotParseSAMLResponse) Error() string {
+ return fmt.Sprintf("cannot parse SAML response: %v", e.Err)
+}
+
+func (e ErrCannotValidateAssertion) Error() string {
+ return fmt.Sprintf("cannot validate assertion: %v", e.Err)
+}
+
+func (e ErrCannotExtractUserAttributes) Error() string {
+ return fmt.Sprintf("cannot extract user attributes: %v", e.Err)
+}
+
+func (e ErrCannotMapRole) Error() string {
+ return fmt.Sprintf("cannot map role: %v", e.Err)
+}
+
+func (e ErrReplayAttackDetected) Error() string {
+ return fmt.Sprintf("replay attack detected for assertion %s: %v", e.AssertionID, e.Err)
+}
+
+func NewSAMLService(
+ pg *pg.Client,
+ encryptionKey cipher.EncryptionKey,
+ baseURL string,
+ sessionDuration time.Duration,
+ cookieName string,
+ cookieSecret string,
+ certificatePEM string,
+ privateKeyPEM string,
+ logger *log.Logger,
+) (*SAMLService, error) {
+ var certificate *x509.Certificate
+ var privateKey *rsa.PrivateKey
+
+ if certificatePEM != "" {
+ block, _ := pem.Decode([]byte(certificatePEM))
+ if block == nil || block.Type != "CERTIFICATE" {
+ return nil, fmt.Errorf("invalid certificate PEM format")
+ }
+
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse certificate: %w", err)
+ }
+ certificate = cert
+ }
+
+ if privateKeyPEM != "" {
+ block, _ := pem.Decode([]byte(privateKeyPEM))
+ if block == nil {
+ return nil, fmt.Errorf("invalid private key PEM format")
+ }
+
+ var key *rsa.PrivateKey
+ var err error
+ switch block.Type {
+ case "RSA PRIVATE KEY":
+ key, err = x509.ParsePKCS1PrivateKey(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse PKCS1 private key: %w", err)
+ }
+ case "PRIVATE KEY":
+ parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse PKCS8 private key: %w", err)
+ }
+ var ok bool
+ key, ok = parsedKey.(*rsa.PrivateKey)
+ if !ok {
+ return nil, fmt.Errorf("private key is not RSA")
+ }
+ default:
+ return nil, fmt.Errorf("unsupported private key type: %s", block.Type)
+ }
+ privateKey = key
+ }
+
+ return &SAMLService{
+ pg: pg,
+ encryptionKey: encryptionKey,
+ baseURL: baseURL,
+ sessionDuration: sessionDuration,
+ cookieName: cookieName,
+ cookieSecret: cookieSecret,
+ certificate: certificate,
+ privateKey: privateKey,
+ logger: logger,
+ }, nil
+}
+
+func (s *SAMLService) GetEntityID() string {
+ return fmt.Sprintf("%s/auth/saml/metadata", s.baseURL)
+}
+
+func (s *SAMLService) GetAcsURL() string {
+ return fmt.Sprintf("%s/auth/saml/consume", s.baseURL)
+}
+
+func parseRawSAMLResponse(encodedResponse string) (*saml.Assertion, error) {
+ rawResponseBuf, err := base64.StdEncoding.DecodeString(encodedResponse)
+ if err != nil {
+ return nil, fmt.Errorf("cannot decode base64: %w", err)
+ }
+
+ var response saml.Response
+ if err := xml.Unmarshal(rawResponseBuf, &response); err != nil {
+ return nil, fmt.Errorf("cannot unmarshal response: %w", err)
+ }
+
+ if response.Assertion == nil {
+ if response.EncryptedAssertion != nil {
+ return nil, fmt.Errorf("response contains encrypted assertion which cannot be parsed without SP private key")
+ }
+ return nil, fmt.Errorf("response contains no assertion")
+ }
+
+ return response.Assertion, nil
+}
+
+func (s *SAMLService) GetServiceProvider(
+ ctx context.Context,
+ config *coredata.SAMLConfiguration,
+) (*saml.ServiceProvider, error) {
+ if s.certificate == nil || s.privateKey == nil {
+ return nil, ErrSPCertificateNotConfigured{}
+ }
+
+ idpCert, err := ParseIdPCertificate(config.IdPCertificate)
+ if err != nil {
+ return nil, ErrInvalidIdPCertificate{Err: err}
+ }
+
+ acsURL, err := url.Parse(s.GetAcsURL())
+ if err != nil {
+ return nil, ErrInvalidURL{Field: "ACS", URL: s.GetAcsURL(), Err: err}
+ }
+
+ idpSSOURL, err := url.Parse(config.IdPSsoURL)
+ if err != nil {
+ return nil, ErrInvalidURL{Field: "IdP SSO", URL: config.IdPSsoURL, Err: err}
+ }
+
+ sp := &saml.ServiceProvider{
+ EntityID: s.GetEntityID(),
+ Key: s.privateKey,
+ Certificate: s.certificate,
+ MetadataURL: *acsURL,
+ AcsURL: *acsURL,
+ SloURL: *acsURL,
+ IDPMetadata: &saml.EntityDescriptor{
+ EntityID: config.IdPEntityID,
+ IDPSSODescriptors: []saml.IDPSSODescriptor{
+ {
+ SSODescriptor: saml.SSODescriptor{
+ RoleDescriptor: saml.RoleDescriptor{
+ ProtocolSupportEnumeration: "urn:oasis:names:tc:SAML:2.0:protocol",
+ KeyDescriptors: []saml.KeyDescriptor{
+ {
+ Use: "signing",
+ KeyInfo: saml.KeyInfo{
+ X509Data: saml.X509Data{
+ X509Certificates: []saml.X509Certificate{
+ {Data: base64.StdEncoding.EncodeToString(idpCert.Raw)},
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ SingleSignOnServices: []saml.Endpoint{
+ {
+ Binding: saml.HTTPRedirectBinding,
+ Location: idpSSOURL.String(),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ return sp, nil
+}
+
+func (s *SAMLService) InitiateSAMLLogin(
+ ctx context.Context,
+ organizationID gid.GID,
+ tenantID gid.TenantID,
+ emailDomain string,
+) (string, error) {
+ var config coredata.SAMLConfiguration
+ scope := coredata.NewScope(tenantID)
+
+ err := s.pg.WithConn(
+ ctx,
+ func(conn pg.Conn) error {
+ return config.LoadByOrganizationIDAndEmailDomain(ctx, conn, scope, organizationID, emailDomain)
+ },
+ )
+ if err != nil {
+ return "", ErrSAMLConfigurationNotFound{OrganizationID: organizationID}
+ }
+
+ if !config.Enabled {
+ return "", ErrSAMLDisabled{OrganizationID: organizationID}
+ }
+
+ sp, err := s.GetServiceProvider(ctx, &config)
+ if err != nil {
+ return "", ErrCannotCreateServiceProvider{Err: err}
+ }
+
+ authReq, err := sp.MakeAuthenticationRequest(
+ config.IdPSsoURL,
+ saml.HTTPRedirectBinding,
+ saml.HTTPPostBinding,
+ )
+ if err != nil {
+ return "", ErrCannotCreateAuthRequest{Err: err}
+ }
+
+ relayStateToken, err := coredata.GenerateSecureToken()
+ if err != nil {
+ return "", fmt.Errorf("cannot generate relay state token: %w", err)
+ }
+
+ now := time.Now()
+ requestExpiry := now.Add(10 * time.Minute)
+ relayStateExpiry := now.Add(15 * time.Minute)
+
+ err = s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ samlRequest := coredata.SAMLRequest{
+ ID: authReq.ID,
+ OrganizationID: organizationID,
+ CreatedAt: now,
+ ExpiresAt: requestExpiry,
+ }
+ if err := samlRequest.Insert(ctx, tx, scope); err != nil {
+ return fmt.Errorf("cannot store SAML request: %w", err)
+ }
+
+ relayState := coredata.SAMLRelayState{
+ Token: relayStateToken,
+ OrganizationID: organizationID,
+ SAMLConfigID: config.ID,
+ RequestID: authReq.ID,
+ CreatedAt: now,
+ ExpiresAt: relayStateExpiry,
+ }
+ if err := relayState.Insert(ctx, tx, scope); err != nil {
+ return fmt.Errorf("cannot store relay state: %w", err)
+ }
+
+ return nil
+ },
+ )
+ if err != nil {
+ return "", err
+ }
+
+ redirectURL, err := authReq.Redirect(relayStateToken, sp)
+ if err != nil {
+ return "", ErrCannotGenerateRedirectURL{Err: err}
+ }
+
+ return redirectURL.String(), nil
+}
+
+type SAMLUserInfo struct {
+ Email string
+ FullName string
+ Role string
+ SAMLSubject string
+ OrganizationID gid.GID
+ TenantID gid.TenantID
+ SAMLConfigID gid.GID
+}
+
+func (s *SAMLService) HandleSAMLAssertion(
+ ctx context.Context,
+ req *http.Request,
+) (*SAMLUserInfo, error) {
+ relayStateToken := req.FormValue("RelayState")
+ if relayStateToken == "" {
+ return nil, fmt.Errorf("missing RelayState in SAML response")
+ }
+
+ var relayState coredata.SAMLRelayState
+ var samlRequest coredata.SAMLRequest
+ var config coredata.SAMLConfiguration
+ var org coredata.Organization
+
+ now := time.Now()
+
+ err := s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ if err := relayState.Load(ctx, tx, relayStateToken); err != nil {
+ return fmt.Errorf("invalid relay state: %w", err)
+ }
+
+ if relayState.IsExpired(now) {
+ return coredata.ErrRelayStateExpired{Token: relayStateToken, ExpiresAt: relayState.ExpiresAt}
+ }
+
+ if err := samlRequest.Load(ctx, tx, relayState.RequestID, relayState.OrganizationID); err != nil {
+ return fmt.Errorf("invalid SAML request: %w", err)
+ }
+
+ if samlRequest.IsExpired(now) {
+ return coredata.ErrSAMLRequestExpired{RequestID: relayState.RequestID, ExpiresAt: samlRequest.ExpiresAt}
+ }
+
+ if err := org.LoadByID(ctx, tx, coredata.NewNoScope(), relayState.OrganizationID); err != nil {
+ return fmt.Errorf("organization not found: %w", err)
+ }
+
+ if err := relayState.Delete(ctx, tx); err != nil {
+ return fmt.Errorf("cannot delete relay state: %w", err)
+ }
+ if err := samlRequest.Delete(ctx, tx); err != nil {
+ return fmt.Errorf("cannot delete SAML request: %w", err)
+ }
+
+ return nil
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ samlResponseEncoded := req.FormValue("SAMLResponse")
+ if samlResponseEncoded == "" {
+ return nil, fmt.Errorf("missing SAMLResponse in request")
+ }
+
+ scope := coredata.NewScope(org.TenantID)
+ err = s.pg.WithConn(
+ ctx,
+ func(conn pg.Conn) error {
+ return config.LoadByID(ctx, conn, scope, relayState.SAMLConfigID)
+ },
+ )
+ if err != nil {
+ return nil, ErrSAMLConfigurationNotFound{OrganizationID: relayState.OrganizationID}
+ }
+
+ if !config.Enabled {
+ return nil, ErrSAMLDisabled{OrganizationID: relayState.OrganizationID}
+ }
+
+ sp, err := s.GetServiceProvider(ctx, &config)
+ if err != nil {
+ return nil, ErrCannotCreateServiceProvider{Err: err}
+ }
+
+ if req.URL.Scheme == "" {
+ req.URL.Scheme = "https"
+ }
+ if req.URL.Host == "" {
+ req.URL.Host = req.Host
+ }
+
+ possibleRequestIDs := []string{samlRequest.ID}
+ assertion, err := sp.ParseResponse(req, possibleRequestIDs)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse SAML response (SP EntityID: %s, IdP EntityID: %s): %w",
+ s.GetEntityID(), config.IdPEntityID, err)
+ }
+
+ if err := ValidateAssertion(assertion, s.GetEntityID(), now); err != nil {
+ return nil, ErrCannotValidateAssertion{Err: err}
+ }
+ if assertion.ID != "" {
+ var expiresAt time.Time
+ if assertion.Conditions != nil && !assertion.Conditions.NotOnOrAfter.IsZero() {
+ expiresAt = assertion.Conditions.NotOnOrAfter
+ } else {
+ expiresAt = now.Add(24 * time.Hour)
+ }
+
+ scope := coredata.NewScope(org.TenantID)
+ err = s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ return PreventReplayAttack(ctx, tx, scope, assertion.ID, relayState.OrganizationID, expiresAt)
+ },
+ )
+ if err != nil {
+ return nil, ErrReplayAttackDetected{AssertionID: assertion.ID, Err: err}
+ }
+ }
+
+ email, fullname, samlRole, err := ExtractUserAttributes(
+ assertion,
+ config.AttributeEmail,
+ config.AttributeFirstname,
+ config.AttributeLastname,
+ config.AttributeRole,
+ )
+ if err != nil {
+ return nil, ErrCannotExtractUserAttributes{Err: err}
+ }
+
+ actualEmailDomain, err := ExtractEmailDomain(email)
+ if err != nil {
+ return nil, ErrCannotExtractUserAttributes{Err: fmt.Errorf("cannot extract domain from email: %w", err)}
+ }
+ if actualEmailDomain != config.EmailDomain {
+ return nil, fmt.Errorf("email domain mismatch: assertion contains email with domain %s but SAML config is for domain %s", actualEmailDomain, config.EmailDomain)
+ }
+
+ systemRole, err := MapSAMLRoleToSystemRole(samlRole, config.DefaultRole)
+ if err != nil {
+ return nil, ErrCannotMapRole{Err: err}
+ }
+
+ samlSubject := ""
+ if assertion.Subject != nil && assertion.Subject.NameID != nil {
+ samlSubject = assertion.Subject.NameID.Value
+ }
+
+ return &SAMLUserInfo{
+ Email: email,
+ FullName: fullname,
+ Role: systemRole,
+ SAMLSubject: samlSubject,
+ OrganizationID: relayState.OrganizationID,
+ TenantID: org.TenantID,
+ SAMLConfigID: relayState.SAMLConfigID,
+ }, nil
+}
+
+func (s *SAMLService) GetMetadataURL(organizationID gid.GID) string {
+ return fmt.Sprintf("%s/auth/saml/metadata/%s", s.baseURL, organizationID)
+}
+
+func (s *SAMLService) GenerateMetadata() ([]byte, error) {
+ if s.certificate == nil {
+ return nil, ErrSPCertificateNotConfigured{}
+ }
+
+ return GenerateServiceProviderMetadata(
+ s.GetEntityID(),
+ s.GetAcsURL(),
+ s.certificate,
+ )
+}
diff --git a/pkg/auth/saml_validator.go b/pkg/auth/saml_validator.go
new file mode 100644
index 000000000..ff93c18fd
--- /dev/null
+++ b/pkg/auth/saml_validator.go
@@ -0,0 +1,110 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/crewjam/saml"
+ "github.com/getprobo/probo/pkg/coredata"
+ "github.com/getprobo/probo/pkg/gid"
+ "go.gearno.de/kit/pg"
+)
+
+func PreventReplayAttack(
+ ctx context.Context,
+ conn pg.Conn,
+ scope coredata.Scoper,
+ assertionID string,
+ organizationID gid.GID,
+ expiresAt time.Time,
+) error {
+ var assertion coredata.SAMLAssertion
+ exists, err := assertion.CheckExists(ctx, conn, assertionID)
+ if err != nil {
+ return fmt.Errorf("failed to check assertion ID: %w", err)
+ }
+
+ if exists {
+ return coredata.ErrAssertionAlreadyUsed{AssertionID: assertionID}
+ }
+
+ now := time.Now()
+ assertion = coredata.SAMLAssertion{
+ ID: assertionID,
+ OrganizationID: organizationID,
+ UsedAt: now,
+ ExpiresAt: expiresAt,
+ }
+
+ if err := assertion.Insert(ctx, conn, scope); err != nil {
+ return fmt.Errorf("failed to store assertion ID: %w", err)
+ }
+
+ return nil
+}
+
+func ValidateAssertion(
+ assertion *saml.Assertion,
+ expectedAudience string,
+ now time.Time,
+) error {
+ const clockSkewTolerance = 5 * time.Minute
+
+ if assertion.Conditions != nil && !assertion.Conditions.NotBefore.IsZero() {
+ if now.Add(clockSkewTolerance).Before(assertion.Conditions.NotBefore) {
+ return fmt.Errorf("assertion not yet valid (NotBefore: %v, now: %v, tolerance: %v)",
+ assertion.Conditions.NotBefore, now, clockSkewTolerance)
+ }
+ }
+
+ if assertion.Conditions != nil && !assertion.Conditions.NotOnOrAfter.IsZero() {
+ if now.Add(-clockSkewTolerance).After(assertion.Conditions.NotOnOrAfter) ||
+ now.Add(-clockSkewTolerance).Equal(assertion.Conditions.NotOnOrAfter) {
+ return fmt.Errorf("assertion expired (NotOnOrAfter: %v, now: %v, tolerance: %v)",
+ assertion.Conditions.NotOnOrAfter, now, clockSkewTolerance)
+ }
+ }
+
+ if assertion.Conditions != nil && len(assertion.Conditions.AudienceRestrictions) > 0 {
+ audienceValid := false
+ for _, restriction := range assertion.Conditions.AudienceRestrictions {
+ if restriction.Audience.Value == expectedAudience {
+ audienceValid = true
+ break
+ }
+ }
+
+ if !audienceValid {
+ return fmt.Errorf("assertion audience restriction does not match expected audience %q", expectedAudience)
+ }
+ }
+
+ return nil
+}
+
+func CleanupExpiredAssertions(ctx context.Context, conn pg.Conn) (int64, error) {
+ return coredata.DeleteExpiredSAMLAssertions(ctx, conn, time.Now())
+}
+
+func CleanupExpiredRequests(ctx context.Context, conn pg.Conn) (int64, error) {
+ return coredata.DeleteExpiredSAMLRequests(ctx, conn, time.Now())
+}
+
+func CleanupExpiredRelayStates(ctx context.Context, conn pg.Conn) (int64, error) {
+ return coredata.DeleteExpiredSAMLRelayStates(ctx, conn, time.Now())
+}
diff --git a/pkg/auth/service.go b/pkg/auth/service.go
index a0618b8b7..6adfb86b8 100644
--- a/pkg/auth/service.go
+++ b/pkg/auth/service.go
@@ -16,17 +16,22 @@ package auth
import (
"context"
+ "crypto/rand"
+ "encoding/hex"
"errors"
"fmt"
+ "net"
"net/mail"
"net/url"
"time"
"github.com/getprobo/probo/packages/emails"
"github.com/getprobo/probo/pkg/coredata"
+ "github.com/getprobo/probo/pkg/crypto/cipher"
"github.com/getprobo/probo/pkg/crypto/passwdhash"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/statelesstoken"
+ "github.com/jackc/pgx/v5"
"go.gearno.de/kit/pg"
)
@@ -35,13 +40,26 @@ type (
// No organization-related logic - that belongs to authz service
Service struct {
pg *pg.Client
+ encryptionKey cipher.EncryptionKey
hp *passwdhash.Profile
hostname string
+ baseURL string
tokenSecret string
disableSignup bool
invitationTokenValidity time.Duration
}
+ // TenantAuthService handles tenant-scoped authentication operations
+ TenantAuthService struct {
+ pg *pg.Client
+ encryptionKey cipher.EncryptionKey
+ hp *passwdhash.Profile
+ hostname string
+ baseURL string
+ tokenSecret string
+ scope coredata.Scoper
+ }
+
ErrInvalidCredentials struct {
message string
}
@@ -137,22 +155,38 @@ func (e ErrSignupDisabled) Error() string {
func NewService(
ctx context.Context,
pgClient *pg.Client,
+ encryptionKey cipher.EncryptionKey,
hp *passwdhash.Profile,
tokenSecret string,
hostname string,
+ baseURL string,
disableSignup bool,
invitationTokenValidity time.Duration,
) (*Service, error) {
return &Service{
pg: pgClient,
+ encryptionKey: encryptionKey,
hp: hp,
hostname: hostname,
+ baseURL: baseURL,
tokenSecret: tokenSecret,
disableSignup: disableSignup,
invitationTokenValidity: invitationTokenValidity,
}, nil
}
+func (s *Service) WithTenant(tenantID gid.TenantID) *TenantAuthService {
+ return &TenantAuthService{
+ pg: s.pg,
+ encryptionKey: s.encryptionKey,
+ hp: s.hp,
+ hostname: s.hostname,
+ baseURL: s.baseURL,
+ tokenSecret: s.tokenSecret,
+ scope: coredata.NewScope(tenantID),
+ }
+}
+
func (s Service) ForgetPassword(
ctx context.Context,
email string,
@@ -265,7 +299,7 @@ func (s Service) SignUp(
err = s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
- if err := user.Insert(ctx, tx); err != nil {
+ if err := user.Insert(ctx, tx, coredata.NewNoScope()); err != nil {
var errUserAlreadyExists *coredata.ErrUserAlreadyExists
if errors.As(err, &errUserAlreadyExists) {
return &ErrUserAlreadyExists{errUserAlreadyExists.Error()}
@@ -328,6 +362,116 @@ func (s Service) SignUp(
return user, session, nil
}
+func (s Service) CreateOrGetSAMLUser(
+ ctx context.Context,
+ emailAddress string,
+ fullName string,
+ samlSubject string,
+) (*coredata.User, error) {
+ if _, err := mail.ParseAddress(emailAddress); err != nil {
+ return nil, &ErrInvalidEmail{emailAddress}
+ }
+
+ if fullName == "" {
+ return nil, &ErrInvalidFullName{fullName}
+ }
+
+ if samlSubject == "" {
+ return nil, fmt.Errorf("SAML subject cannot be empty")
+ }
+
+ var user coredata.User
+ now := time.Now()
+
+ err := s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ // Try to load existing user by email
+ if err := user.LoadByEmail(ctx, tx, emailAddress); err == nil {
+ // User exists - update SAML subject and full name if needed
+ needsUpdate := false
+
+ if user.SAMLSubject == nil || *user.SAMLSubject != samlSubject {
+ user.SAMLSubject = &samlSubject
+ needsUpdate = true
+ }
+ if user.FullName != fullName {
+ user.FullName = fullName
+ needsUpdate = true
+ }
+ if !user.EmailAddressVerified {
+ user.EmailAddressVerified = true
+ needsUpdate = true
+ }
+
+ if needsUpdate {
+ user.UpdatedAt = now
+ if err := user.Update(ctx, tx); err != nil {
+ return fmt.Errorf("cannot update user: %w", err)
+ }
+ }
+ return nil
+ }
+
+ // No existing user, create new user (all users are global now)
+ user = coredata.User{
+ ID: gid.New(gid.NilTenant, coredata.UserEntityType),
+ EmailAddress: emailAddress,
+ HashedPassword: nil, // SAML users don't have passwords initially
+ EmailAddressVerified: true, // SAML users are verified by IdP
+ FullName: fullName,
+ SAMLSubject: &samlSubject,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ if err := user.Insert(ctx, tx, coredata.NewNoScope()); err != nil {
+ return fmt.Errorf("cannot insert SAML user: %w", err)
+ }
+
+ return nil
+ },
+ )
+
+ if err != nil {
+ return nil, err
+ }
+
+ return &user, nil
+}
+
+func (s Service) CreateSessionForUser(
+ ctx context.Context,
+ userID gid.GID,
+ sessionDuration time.Duration,
+) (*coredata.Session, error) {
+ now := time.Now()
+ session := &coredata.Session{
+ ID: gid.New(gid.NilTenant, coredata.SessionEntityType),
+ UserID: userID,
+ Data: coredata.SessionData{},
+ ExpiredAt: now.Add(sessionDuration),
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ err := s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ if err := session.Insert(ctx, tx); err != nil {
+ return fmt.Errorf("cannot insert session: %w", err)
+ }
+ return nil
+ },
+ )
+
+ if err != nil {
+ return nil, err
+ }
+
+ return session, nil
+}
+
func (s Service) SignIn(
ctx context.Context,
emailAddress string,
@@ -340,6 +484,69 @@ func (s Service) SignIn(
user := &coredata.User{}
session := &coredata.Session{}
+ err := s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ // Load user by email (all users are global now)
+ if err := user.LoadByEmail(ctx, tx, emailAddress); err != nil {
+ var errUserNotFound *coredata.ErrUserNotFound
+ if errors.As(err, &errUserNotFound) {
+ return &ErrInvalidCredentials{"invalid email or password"}
+ }
+ return fmt.Errorf("cannot load user by email: %w", err)
+ }
+
+ // Verify password
+ match, err := s.hp.ComparePasswordAndHash([]byte(password), user.HashedPassword)
+ if err != nil {
+ return fmt.Errorf("cannot verify password: %w", err)
+ }
+ if !match {
+ return &ErrInvalidCredentials{"invalid email or password"}
+ }
+
+ // Create new session with password authentication flag set
+ now := time.Now()
+ session = &coredata.Session{
+ ID: gid.New(gid.NilTenant, coredata.SessionEntityType),
+ UserID: user.ID,
+ Data: coredata.SessionData{
+ PasswordAuthenticated: true,
+ SAMLAuthenticatedOrgs: make(map[string]coredata.SAMLAuthInfo),
+ },
+ ExpiredAt: now.Add(24 * time.Hour * 7), // 7 days
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ if err := session.Insert(ctx, tx); err != nil {
+ return fmt.Errorf("cannot insert session: %w", err)
+ }
+
+ return nil
+ },
+ )
+
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return session, user, nil
+}
+
+func (s Service) SignInWithExistingSession(
+ ctx context.Context,
+ emailAddress string,
+ password string,
+ existingSession *coredata.Session,
+) (*coredata.Session, *coredata.User, error) {
+ if _, err := mail.ParseAddress(emailAddress); err != nil {
+ return nil, nil, &ErrInvalidCredentials{"invalid email or password"}
+ }
+
+ user := &coredata.User{}
+ session := &coredata.Session{}
+
err := s.pg.WithTx(
ctx,
func(tx pg.Conn) error {
@@ -359,18 +566,38 @@ func (s Service) SignIn(
return &ErrInvalidCredentials{"invalid email or password"}
}
- now := time.Now()
- session = &coredata.Session{
- ID: gid.New(gid.NilTenant, coredata.SessionEntityType),
- UserID: user.ID,
- Data: coredata.SessionData{},
- ExpiredAt: now.Add(24 * time.Hour * 7), // 7 days
- CreatedAt: now,
- UpdatedAt: now,
- }
+ if existingSession != nil && existingSession.UserID == user.ID {
+ session = &coredata.Session{}
+ if err := session.LoadByID(ctx, tx, existingSession.ID); err != nil {
+ return fmt.Errorf("cannot load session: %w", err)
+ }
- if err := session.Insert(ctx, tx); err != nil {
- return fmt.Errorf("cannot insert session: %w", err)
+ session.Data.PasswordAuthenticated = true
+ if session.Data.SAMLAuthenticatedOrgs == nil {
+ session.Data.SAMLAuthenticatedOrgs = make(map[string]coredata.SAMLAuthInfo)
+ }
+ session.UpdatedAt = time.Now()
+
+ if err := session.Update(ctx, tx); err != nil {
+ return fmt.Errorf("cannot update session: %w", err)
+ }
+ } else {
+ now := time.Now()
+ session = &coredata.Session{
+ ID: gid.New(gid.NilTenant, coredata.SessionEntityType),
+ UserID: user.ID,
+ Data: coredata.SessionData{
+ PasswordAuthenticated: true,
+ SAMLAuthenticatedOrgs: make(map[string]coredata.SAMLAuthInfo),
+ },
+ ExpiredAt: now.Add(24 * time.Hour * 7),
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ if err := session.Insert(ctx, tx); err != nil {
+ return fmt.Errorf("cannot insert session: %w", err)
+ }
}
return nil
@@ -510,6 +737,30 @@ func (s Service) UpdateSession(ctx context.Context, sessionID gid.GID) (*coredat
return session, nil
}
+func (s Service) UpdateSessionData(ctx context.Context, sessionID gid.GID, data coredata.SessionData) error {
+ return s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ session := &coredata.Session{}
+ if err := session.LoadByID(ctx, tx, sessionID); err != nil {
+ return &ErrSessionNotFound{"session not found"}
+ }
+
+ if time.Now().After(session.ExpiredAt) {
+ return &ErrSessionExpired{"session expired"}
+ }
+
+ session.Data = data
+ session.UpdatedAt = time.Now()
+ if err := session.Update(ctx, tx); err != nil {
+ return fmt.Errorf("cannot update session: %w", err)
+ }
+
+ return nil
+ },
+ )
+}
+
func (s Service) ConfirmEmail(ctx context.Context, tokenString string) error {
payload, err := statelesstoken.ValidateToken[EmailConfirmationData](
s.tokenSecret,
@@ -655,7 +906,7 @@ func (s Service) SignupFromInvitation(
UpdatedAt: now,
}
- if err := user.Insert(ctx, tx); err != nil {
+ if err := user.Insert(ctx, tx, coredata.NewNoScope()); err != nil {
var errUserAlreadyExists *coredata.ErrUserAlreadyExists
if errors.As(err, &errUserAlreadyExists) {
return &ErrUserAlreadyExists{errUserAlreadyExists.Error()}
@@ -686,3 +937,342 @@ func (s Service) SignupFromInvitation(
return user, session, nil
}
+
+// IsTenantUser removed - all users are now global (no tenant distinction)
+
+func (s Service) GetUserAuthMethod(
+ ctx context.Context,
+ scope coredata.Scoper,
+ userID gid.GID,
+ organizationID gid.GID,
+ session *coredata.Session,
+) (coredata.UserAuthMethod, error) {
+ // Load the user to check their email and SAML subject
+ user := &coredata.User{}
+ err := s.pg.WithConn(ctx, func(conn pg.Conn) error {
+ return user.LoadByID(ctx, conn, userID)
+ })
+ if err != nil {
+ return "", fmt.Errorf("cannot load user: %w", err)
+ }
+
+ // If user doesn't have a SAML subject, they only use password auth
+ if user.SAMLSubject == nil || *user.SAMLSubject == "" {
+ return coredata.UserAuthMethodPassword, nil
+ }
+
+ // User has SAML subject - check if there's SAML config for this org + user's domain
+ // Extract domain from user email
+ emailParts := []byte(user.EmailAddress)
+ atIndex := -1
+ for i, b := range emailParts {
+ if b == '@' {
+ atIndex = i
+ break
+ }
+ }
+ if atIndex == -1 {
+ return coredata.UserAuthMethodPassword, nil
+ }
+ domain := string(emailParts[atIndex+1:])
+
+ // Check if SAML is configured for this org + domain
+ var samlConfig coredata.SAMLConfiguration
+ orgScope := coredata.NewScope(organizationID.TenantID())
+ err = s.pg.WithConn(ctx, func(conn pg.Conn) error {
+ err := samlConfig.LoadByOrganizationIDAndEmailDomain(ctx, conn, orgScope, organizationID, domain)
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return nil // No SAML config for this org+domain
+ }
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ return "", fmt.Errorf("cannot check SAML configuration: %w", err)
+ }
+
+ // If SAML config exists for this org+domain, user enrolled via SAML
+ if samlConfig.ID != (gid.GID{}) {
+ return coredata.UserAuthMethodSAML, nil
+ }
+
+ // No SAML config for this org, user uses password
+ return coredata.UserAuthMethodPassword, nil
+}
+
+// Organization Access Control
+
+type (
+ // ErrSAMLAuthRequired indicates user must authenticate via SAML to access org
+ ErrSAMLAuthRequired struct {
+ ConfigID gid.GID
+ OrganizationID gid.GID
+ RedirectURL string // SAML IdP login URL
+ }
+
+ // ErrPasswordAuthRequired indicates user must authenticate with password to access org
+ ErrPasswordAuthRequired struct {
+ OrganizationID gid.GID
+ RedirectURL string // Password login page URL
+ }
+)
+
+func (e ErrSAMLAuthRequired) Error() string {
+ return "SAML authentication required for this organization"
+}
+
+func (e ErrPasswordAuthRequired) Error() string {
+ return "password authentication required for this organization"
+}
+
+// CheckOrganizationAccess determines if a user can access an organization
+// based on SAML configuration and session authentication state
+func (s Service) CheckOrganizationAccess(
+ ctx context.Context,
+ user *coredata.User,
+ organizationID gid.GID,
+ session *coredata.Session,
+) error {
+ // Extract domain from user email
+ emailParts := []byte(user.EmailAddress)
+ atIndex := -1
+ for i, b := range emailParts {
+ if b == '@' {
+ atIndex = i
+ break
+ }
+ }
+ if atIndex == -1 {
+ return fmt.Errorf("invalid email address format")
+ }
+ domain := string(emailParts[atIndex+1:])
+
+ // Find SAML configuration for this organization and domain
+ var samlConfig coredata.SAMLConfiguration
+ scope := coredata.NewScope(organizationID.TenantID())
+ err := s.pg.WithConn(ctx, func(conn pg.Conn) error {
+ err := samlConfig.LoadByOrganizationIDAndEmailDomain(ctx, conn, scope, organizationID, domain)
+ if err != nil {
+ // If no SAML config found for this organization and domain, that's okay - not an error
+ // Just means this organization doesn't have SAML configured for this domain
+ if errors.Is(err, pgx.ErrNoRows) {
+ return nil
+ }
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("cannot check SAML configuration: %w", err)
+ }
+
+ // Check if SAML is configured and enabled for this domain and organization
+ if samlConfig.ID != (gid.GID{}) && samlConfig.Enabled && samlConfig.DomainVerified {
+ // SAML config exists for this org - check enforcement policy
+ if samlConfig.EnforcementPolicy == coredata.SAMLEnforcementPolicyRequired {
+ // SAML is REQUIRED - check if user has SAML-authenticated for this org
+ authInfo, hasSAMLAuth := session.Data.SAMLAuthenticatedOrgs[organizationID.String()]
+ if !hasSAMLAuth {
+ // Build SAML login URL
+ samlLoginURL := fmt.Sprintf("%s/auth/saml/login/%s", s.baseURL, samlConfig.ID)
+ return ErrSAMLAuthRequired{
+ ConfigID: samlConfig.ID,
+ OrganizationID: organizationID,
+ RedirectURL: samlLoginURL,
+ }
+ }
+
+ // Optional: Check if SAML auth is still recent (not too old)
+ // For now, we trust the session lifetime
+ _ = authInfo
+ } else {
+ // SAML is OPTIONAL or OFF - allow either password OR SAML auth for this specific org
+ hasSAMLAuth := false
+ if _, ok := session.Data.SAMLAuthenticatedOrgs[organizationID.String()]; ok {
+ hasSAMLAuth = true
+ }
+
+ if !session.Data.PasswordAuthenticated && !hasSAMLAuth {
+ // User needs to authenticate - offer SAML as option
+ samlLoginURL := fmt.Sprintf("%s/auth/saml/login/%s", s.baseURL, samlConfig.ID)
+ return ErrSAMLAuthRequired{
+ ConfigID: samlConfig.ID,
+ OrganizationID: organizationID,
+ RedirectURL: samlLoginURL,
+ }
+ }
+ }
+ } else {
+ // No SAML configuration for this org+domain combination
+ // Require password authentication for password-only organizations
+ if !session.Data.PasswordAuthenticated {
+ // User hasn't authenticated with password - require password authentication
+ loginURL := fmt.Sprintf("%s/authentication/login?method=password", s.baseURL)
+ return ErrPasswordAuthRequired{
+ OrganizationID: organizationID,
+ RedirectURL: loginURL,
+ }
+ }
+ }
+
+ return nil // Access granted
+}
+
+// InitiateDomainVerification creates a SAML configuration with unverified domain and generates verification token
+func (s Service) InitiateDomainVerification(
+ ctx context.Context,
+ tenantID gid.TenantID,
+ organizationID gid.GID,
+ emailDomain string,
+) (*coredata.SAMLConfiguration, error) {
+ token, err := GenerateDomainVerificationToken()
+ if err != nil {
+ return nil, fmt.Errorf("cannot generate verification token: %w", err)
+ }
+
+ var config *coredata.SAMLConfiguration
+
+ err = s.pg.WithTx(ctx, func(tx pg.Conn) error {
+ now := time.Now()
+ scope := coredata.NewScope(tenantID)
+
+ config = &coredata.SAMLConfiguration{
+ ID: gid.New(tenantID, coredata.SAMLConfigurationEntityType),
+ OrganizationID: organizationID,
+ EmailDomain: emailDomain,
+ Enabled: false,
+ EnforcementPolicy: coredata.SAMLEnforcementPolicyOff,
+ DomainVerified: false,
+ DomainVerificationToken: &token,
+ // Default IdP values (placeholders until configured)
+ IdPEntityID: "not-configured",
+ IdPSsoURL: "not-configured",
+ IdPCertificate: "not-configured",
+ // Default attribute mappings
+ AttributeEmail: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
+ AttributeFirstname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
+ AttributeLastname: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
+ AttributeRole: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
+ DefaultRole: "MEMBER",
+ AutoSignupEnabled: false,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ if err := config.Insert(ctx, tx, scope); err != nil {
+ return fmt.Errorf("cannot insert SAML configuration: %w", err)
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ return config, nil
+}
+
+// VerifyDomain checks DNS TXT record and marks domain as verified if found
+func (s Service) VerifyDomain(
+ ctx context.Context,
+ tenantID gid.TenantID,
+ configID gid.GID,
+) (*coredata.SAMLConfiguration, bool, error) {
+ var config *coredata.SAMLConfiguration
+ var verified bool
+
+ err := s.pg.WithTx(ctx, func(tx pg.Conn) error {
+ scope := coredata.NewScope(tenantID)
+
+ // Load config
+ config = &coredata.SAMLConfiguration{}
+ if err := config.LoadByID(ctx, tx, scope, configID); err != nil {
+ return fmt.Errorf("cannot load SAML configuration: %w", err)
+ }
+
+ if config.DomainVerificationToken == nil {
+ return fmt.Errorf("no verification token found for this configuration")
+ }
+
+ if config.DomainVerified {
+ verified = true
+ return nil // Already verified
+ }
+
+ // Check DNS TXT record
+ isVerified, err := VerifyDomainOwnership(ctx, config.EmailDomain, *config.DomainVerificationToken)
+ if err != nil {
+ return fmt.Errorf("cannot verify domain ownership: %w", err)
+ }
+
+ verified = isVerified
+
+ if isVerified {
+ now := time.Now()
+ config.DomainVerified = true
+ config.DomainVerifiedAt = &now
+ config.UpdatedAt = now
+
+ if err := config.Update(ctx, tx, scope); err != nil {
+ return fmt.Errorf("cannot update SAML configuration: %w", err)
+ }
+ }
+
+ return nil
+ })
+
+ if err != nil {
+ return nil, false, err
+ }
+
+ return config, verified, nil
+}
+
+// Domain Verification Methods
+
+// GenerateDomainVerificationToken generates a random 32-character hex token for domain verification
+func GenerateDomainVerificationToken() (string, error) {
+ bytes := make([]byte, 16) // 16 bytes = 32 hex characters
+ if _, err := rand.Read(bytes); err != nil {
+ return "", fmt.Errorf("cannot generate domain verification token: %w", err)
+ }
+ return hex.EncodeToString(bytes), nil
+}
+
+// GetDomainVerificationRecord returns the DNS TXT record string that should be added to the domain
+func GetDomainVerificationRecord(token string) string {
+ return fmt.Sprintf("probo-verification=%s", token)
+}
+
+// VerifyDomainOwnership performs DNS lookup to verify domain ownership via TXT record
+func VerifyDomainOwnership(ctx context.Context, domain, expectedToken string) (bool, error) {
+ // Use net package for DNS TXT record lookup
+ var txtRecords []string
+ var err error
+
+ // Create a DNS resolver with timeout from context
+ resolver := &net.Resolver{
+ PreferGo: true,
+ }
+
+ txtRecords, err = resolver.LookupTXT(ctx, domain)
+ if err != nil {
+ // DNS lookup errors are expected if the domain doesn't exist or has no TXT records
+ // We return false (not verified) but not an error, as this is a normal case
+ return false, nil
+ }
+
+ // Check if any TXT record matches our verification token
+ expectedRecord := GetDomainVerificationRecord(expectedToken)
+ for _, record := range txtRecords {
+ if record == expectedRecord {
+ return true, nil
+ }
+ }
+
+ // Token not found in DNS records
+ return false, nil
+}
diff --git a/pkg/authz/service.go b/pkg/authz/service.go
index 9a46ecb7d..522bba582 100644
--- a/pkg/authz/service.go
+++ b/pkg/authz/service.go
@@ -261,6 +261,61 @@ func (s *Service) AcceptInvitationByID(
return acceptedInvitation, nil
}
+// EnsureSAMLMembership creates or updates a user's membership in an organization.
+// This is used during SAML authentication to ensure the user has the correct role.
+// This method is on Service (not TenantAuthzService) because SAML authentication
+// happens before the user has tenant access.
+func (s *Service) EnsureSAMLMembership(
+ ctx context.Context,
+ tenantID gid.TenantID,
+ userID gid.GID,
+ organizationID gid.GID,
+ role string,
+) error {
+ scope := coredata.NewScope(tenantID)
+ now := time.Now()
+
+ return s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ var membership coredata.Membership
+
+ // Try to load existing membership
+ err := membership.LoadByUserAndOrg(ctx, tx, scope, userID, organizationID)
+ if err != nil {
+ // Membership doesn't exist, create it
+ membershipID := gid.New(tenantID, coredata.MembershipEntityType)
+ membership = coredata.Membership{
+ ID: membershipID,
+ UserID: userID,
+ OrganizationID: organizationID,
+ Role: role,
+ CreatedAt: now,
+ UpdatedAt: now,
+ }
+
+ if err := membership.Create(ctx, tx, scope); err != nil {
+ return fmt.Errorf("failed to create membership: %w", err)
+ }
+
+ return nil
+ }
+
+ // Membership exists, update role if changed
+ if membership.Role != role {
+ membership.Role = role
+ membership.UpdatedAt = now
+
+ if err := membership.Update(ctx, tx, scope); err != nil {
+ return fmt.Errorf("failed to update membership role: %w", err)
+ }
+ }
+
+ return nil
+ },
+ )
+}
+
// This method is on Service (not TenantAuthzService) because the user viewing
// their invitations doesn't have tenant access yet, and it operates across multiple tenants.
func (s *Service) GetUserInvitations(
diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go
index 2f118e08f..576d6f2ce 100644
--- a/pkg/coredata/entity_type_reg.go
+++ b/pkg/coredata/entity_type_reg.go
@@ -63,4 +63,5 @@ const (
MembershipEntityType
SlackMessageEntityType
TrustCenterFileEntityType
+ SAMLConfigurationEntityType
)
diff --git a/pkg/coredata/membership.go b/pkg/coredata/membership.go
index 012af37e9..7c412234d 100644
--- a/pkg/coredata/membership.go
+++ b/pkg/coredata/membership.go
@@ -132,22 +132,33 @@ func (m *Membership) LoadByID(
membershipID gid.GID,
) error {
query := `
+WITH mbr AS (
+ SELECT
+ id,
+ user_id,
+ organization_id,
+ role,
+ created_at,
+ updated_at
+ FROM
+ authz_memberships
+ WHERE
+ id = @membership_id
+ AND %s
+)
SELECT
- m.id,
- m.user_id,
- m.organization_id,
- m.role,
+ mbr.id,
+ mbr.user_id,
+ mbr.organization_id,
+ mbr.role,
u.fullname as full_name,
u.email_address,
- m.created_at,
- m.updated_at
+ mbr.created_at,
+ mbr.updated_at
FROM
- authz_memberships m
+ mbr
JOIN
- users u ON m.user_id = u.id
-WHERE
- m.id = @membership_id
- AND %s
+ users u ON mbr.user_id = u.id
`
query = fmt.Sprintf(query, scope.SQLFragment())
@@ -182,23 +193,34 @@ func (m *Membership) LoadByUserAndOrg(
orgID gid.GID,
) error {
query := `
+WITH mbr AS (
+ SELECT
+ id,
+ user_id,
+ organization_id,
+ role,
+ created_at,
+ updated_at
+ FROM
+ authz_memberships
+ WHERE
+ user_id = @user_id
+ AND organization_id = @organization_id
+ AND %s
+)
SELECT
- m.id,
- m.user_id,
- m.organization_id,
- m.role,
+ mbr.id,
+ mbr.user_id,
+ mbr.organization_id,
+ mbr.role,
u.fullname as full_name,
u.email_address,
- m.created_at,
- m.updated_at
+ mbr.created_at,
+ mbr.updated_at
FROM
- authz_memberships m
+ mbr
JOIN
- users u ON m.user_id = u.id
-WHERE
- m.user_id = @user_id
- AND m.organization_id = @organization_id
- AND %s
+ users u ON mbr.user_id = u.id
`
query = fmt.Sprintf(query, scope.SQLFragment())
@@ -294,24 +316,35 @@ func (m *Memberships) LoadByUserID(
userID gid.GID,
) error {
query := `
+WITH mbr AS (
+ SELECT
+ id,
+ user_id,
+ organization_id,
+ role,
+ created_at,
+ updated_at
+ FROM
+ authz_memberships
+ WHERE
+ user_id = @user_id
+ AND %s
+ ORDER BY
+ created_at DESC
+)
SELECT
- m.id,
- m.user_id,
- m.organization_id,
- m.role,
+ mbr.id,
+ mbr.user_id,
+ mbr.organization_id,
+ mbr.role,
u.fullname as full_name,
u.email_address,
- m.created_at,
- m.updated_at
+ mbr.created_at,
+ mbr.updated_at
FROM
- authz_memberships m
+ mbr
JOIN
- users u ON m.user_id = u.id
-WHERE
- m.user_id = @user_id
- AND %s
-ORDER BY
- m.created_at DESC
+ users u ON mbr.user_id = u.id
`
query = fmt.Sprintf(query, scope.SQLFragment())
@@ -343,23 +376,45 @@ func (m *Memberships) LoadByOrganizationID(
cursor *page.Cursor[MembershipOrderField],
) error {
query := `
+WITH mbr AS (
+ SELECT
+ id,
+ user_id,
+ organization_id,
+ role,
+ created_at,
+ updated_at
+ FROM
+ authz_memberships
+ WHERE
+ organization_id = @organization_id
+ AND %s
+)
SELECT
- m.id,
- m.user_id,
- m.organization_id,
- m.role,
- u.fullname as full_name,
- u.email_address,
- m.created_at,
- m.updated_at
-FROM
- authz_memberships m
-JOIN
- users u ON m.user_id = u.id
-WHERE
- m.organization_id = @organization_id
- AND %s
- AND %s
+ id,
+ user_id,
+ organization_id,
+ role,
+ full_name,
+ email_address,
+ created_at,
+ updated_at
+FROM (
+ SELECT
+ mbr.id,
+ mbr.user_id,
+ mbr.organization_id,
+ mbr.role,
+ u.fullname as full_name,
+ u.email_address,
+ mbr.created_at,
+ mbr.updated_at
+ FROM
+ mbr
+ JOIN
+ users u ON mbr.user_id = u.id
+) AS membership_with_user
+WHERE %s
`
query = fmt.Sprintf(query, scope.SQLFragment(), cursor.SQLFragment())
@@ -411,3 +466,67 @@ WHERE
}
return count, nil
}
+
+func LoadUserIDsByOrganizationID(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+ organizationID gid.GID,
+) ([]gid.GID, error) {
+ query := `
+SELECT user_id
+FROM authz_memberships
+WHERE organization_id = @organization_id AND %s
+`
+ query = fmt.Sprintf(query, scope.SQLFragment())
+ args := pgx.StrictNamedArgs{"organization_id": organizationID}
+ maps.Copy(args, scope.SQLArguments())
+
+ rows, err := conn.Query(ctx, query, args)
+ if err != nil {
+ return nil, fmt.Errorf("cannot query memberships: %w", err)
+ }
+
+ var userIDs []gid.GID
+ for rows.Next() {
+ var userID gid.GID
+ if err := rows.Scan(&userID); err != nil {
+ rows.Close()
+ return nil, fmt.Errorf("cannot scan user_id: %w", err)
+ }
+ userIDs = append(userIDs, userID)
+ }
+ rows.Close()
+
+ return userIDs, nil
+}
+
+func UpdateMembershipUserID(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+ oldUserID gid.GID,
+ newUserID gid.GID,
+ organizationID gid.GID,
+) error {
+ query := `
+UPDATE authz_memberships
+SET user_id = @new_user_id, updated_at = @updated_at
+WHERE user_id = @old_user_id AND organization_id = @organization_id AND %s
+`
+ query = fmt.Sprintf(query, scope.SQLFragment())
+ args := pgx.StrictNamedArgs{
+ "new_user_id": newUserID,
+ "old_user_id": oldUserID,
+ "organization_id": organizationID,
+ "updated_at": time.Now(),
+ }
+ maps.Copy(args, scope.SQLArguments())
+
+ _, err := conn.Exec(ctx, query, args)
+ if err != nil {
+ return fmt.Errorf("cannot update membership: %w", err)
+ }
+
+ return nil
+}
diff --git a/pkg/coredata/membership_order_field.go b/pkg/coredata/membership_order_field.go
index 6c0b00e3b..334be5eb9 100644
--- a/pkg/coredata/membership_order_field.go
+++ b/pkg/coredata/membership_order_field.go
@@ -28,13 +28,13 @@ const (
func (p MembershipOrderField) Column() string {
switch p {
case MembershipOrderFieldFullName:
- return "u.fullname"
+ return "full_name"
case MembershipOrderFieldEmailAddress:
- return "u.email_address"
+ return "email_address"
case MembershipOrderFieldRole:
- return "m.role"
+ return "role"
case MembershipOrderFieldCreatedAt:
- return "m.created_at"
+ return "created_at"
}
return string(p)
}
diff --git a/pkg/coredata/migrations/20251018T194142Z.sql b/pkg/coredata/migrations/20251018T194142Z.sql
new file mode 100644
index 000000000..657bdb474
--- /dev/null
+++ b/pkg/coredata/migrations/20251018T194142Z.sql
@@ -0,0 +1,155 @@
+-- Add SAML authentication support
+-- This migration adds SAML SSO functionality including:
+-- - SAML configurations per organization
+-- - SAML request/assertion tracking for security
+-- - Domain verification
+-- - User SAML subject tracking
+
+-- Create ENUM for SAML enforcement policies
+CREATE TYPE saml_enforcement_policy AS ENUM (
+ 'OFF', -- SAML disabled, must use password
+ 'OPTIONAL', -- SAML available but not required (default)
+ 'REQUIRED' -- Everyone must use SAML
+);
+
+-- Create auth_saml_configurations table
+CREATE TABLE auth_saml_configurations (
+ id TEXT PRIMARY KEY,
+ tenant_id TEXT NOT NULL,
+ organization_id TEXT NOT NULL,
+ email_domain TEXT NOT NULL,
+
+ -- SAML enabled flag
+ enabled BOOLEAN NOT NULL DEFAULT false,
+
+ -- Enforcement policy for this SAML configuration
+ enforcement_policy saml_enforcement_policy NOT NULL,
+
+ -- Identity Provider (IdP) configuration
+ idp_entity_id TEXT NOT NULL,
+ idp_sso_url TEXT NOT NULL,
+ idp_certificate TEXT NOT NULL, -- X.509 certificate (PEM format)
+ idp_metadata_url TEXT, -- Optional: for auto-refresh
+
+ -- Attribute mapping configuration (using WS-Federation Claims)
+ attribute_email TEXT NOT NULL DEFAULT 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress',
+ attribute_firstname TEXT NOT NULL DEFAULT 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname',
+ attribute_lastname TEXT NOT NULL DEFAULT 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname',
+ attribute_role TEXT NOT NULL DEFAULT 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role',
+
+ -- Default role if mapping fails or attribute missing
+ default_role TEXT NOT NULL DEFAULT 'MEMBER',
+
+ -- Auto-signup settings
+ auto_signup_enabled BOOLEAN NOT NULL DEFAULT false,
+
+ -- Domain verification fields
+ domain_verified BOOLEAN NOT NULL DEFAULT false,
+ domain_verification_token TEXT,
+ domain_verified_at TIMESTAMP,
+
+ -- Timestamps
+ created_at TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP NOT NULL,
+
+ CONSTRAINT fk_auth_saml_configurations_organization FOREIGN KEY (organization_id)
+ REFERENCES organizations(id) ON DELETE CASCADE
+);
+
+-- Index for fast organization lookup
+CREATE INDEX idx_auth_saml_configurations_organization_id
+ ON auth_saml_configurations(organization_id);
+
+-- Index for tenant scoping
+CREATE INDEX idx_auth_saml_configurations_tenant_id
+ ON auth_saml_configurations(tenant_id);
+
+-- Unique constraint scoped to organization
+-- This allows the same domain in different organizations
+-- while preventing duplicates within the same organization
+CREATE UNIQUE INDEX idx_saml_config_domain_org_unique
+ ON auth_saml_configurations(organization_id, email_domain)
+ WHERE enabled = true AND domain_verified = true;
+
+-- Index for fast domain lookup (for email-based SAML discovery)
+CREATE INDEX idx_saml_config_email_domain
+ ON auth_saml_configurations(email_domain)
+ WHERE enabled = true AND domain_verified = true;
+
+-- Add SAML subject to users table for tracking SAML NameID
+ALTER TABLE users ADD COLUMN saml_subject TEXT;
+
+-- Unique constraint: one SAML subject globally
+CREATE UNIQUE INDEX idx_users_saml_subject
+ ON users(saml_subject)
+ WHERE saml_subject IS NOT NULL;
+
+-- Make hashed_password nullable for SAML users
+-- SAML users authenticate via IdP and don't have passwords
+ALTER TABLE users ALTER COLUMN hashed_password DROP NOT NULL;
+
+-- Create auth_saml_assertions table for replay attack prevention
+CREATE TABLE auth_saml_assertions (
+ id TEXT PRIMARY KEY, -- SAML Assertion ID
+ tenant_id TEXT NOT NULL,
+ organization_id TEXT NOT NULL,
+ used_at TIMESTAMP NOT NULL,
+ expires_at TIMESTAMP NOT NULL
+);
+
+-- Index for cleanup of expired assertions
+CREATE INDEX idx_auth_saml_assertions_expires_at
+ ON auth_saml_assertions(expires_at);
+
+-- Index for organization lookup
+CREATE INDEX idx_auth_saml_assertions_organization_id
+ ON auth_saml_assertions(organization_id);
+
+-- Index for tenant scoping
+CREATE INDEX idx_auth_saml_assertions_tenant_id
+ ON auth_saml_assertions(tenant_id);
+
+-- Create auth_saml_requests table for proper InResponseTo validation
+-- This prevents replay attacks and validates the SAML authentication flow
+CREATE TABLE auth_saml_requests (
+ id TEXT PRIMARY KEY, -- SAML Request ID generated by SP
+ organization_id TEXT NOT NULL,
+ tenant_id TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ expires_at TIMESTAMP NOT NULL,
+
+ CONSTRAINT fk_auth_saml_requests_organization FOREIGN KEY (organization_id)
+ REFERENCES organizations(id) ON DELETE CASCADE
+);
+
+-- Index for fast request ID lookup during SAML callback
+CREATE INDEX idx_auth_saml_requests_id_org ON auth_saml_requests(id, organization_id);
+
+-- Index for cleanup of expired requests
+CREATE INDEX idx_auth_saml_requests_expires_at ON auth_saml_requests(expires_at);
+
+-- Create auth_saml_relay_states table for secure RelayState management
+-- This prevents organization hijacking attacks
+CREATE TABLE auth_saml_relay_states (
+ token TEXT PRIMARY KEY, -- Cryptographically secure random token
+ tenant_id TEXT NOT NULL,
+ organization_id TEXT NOT NULL,
+ request_id TEXT NOT NULL, -- Links to auth_saml_requests.id
+ saml_config_id TEXT NOT NULL, -- Links to auth_saml_configurations.id
+ created_at TIMESTAMP NOT NULL,
+ expires_at TIMESTAMP NOT NULL,
+
+ CONSTRAINT fk_auth_saml_relay_states_organization FOREIGN KEY (organization_id)
+ REFERENCES organizations(id) ON DELETE CASCADE,
+ CONSTRAINT fk_auth_saml_relay_states_saml_config FOREIGN KEY (saml_config_id)
+ REFERENCES auth_saml_configurations(id) ON DELETE CASCADE
+);
+
+-- Index for fast token lookup during callback
+CREATE INDEX idx_auth_saml_relay_states_token ON auth_saml_relay_states(token);
+
+-- Index for cleanup of expired relay states
+CREATE INDEX idx_auth_saml_relay_states_expires_at ON auth_saml_relay_states(expires_at);
+
+-- Index for tenant scoping
+CREATE INDEX idx_auth_saml_relay_states_tenant_id ON auth_saml_relay_states(tenant_id);
diff --git a/pkg/coredata/saml_assertion.go b/pkg/coredata/saml_assertion.go
new file mode 100644
index 000000000..7d9f645b8
--- /dev/null
+++ b/pkg/coredata/saml_assertion.go
@@ -0,0 +1,108 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 coredata
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/getprobo/probo/pkg/gid"
+ "github.com/jackc/pgx/v5"
+ "go.gearno.de/kit/pg"
+)
+
+type SAMLAssertion struct {
+ ID string `db:"id"`
+ OrganizationID gid.GID `db:"organization_id"`
+ UsedAt time.Time `db:"used_at"`
+ ExpiresAt time.Time `db:"expires_at"`
+}
+
+type ErrAssertionAlreadyUsed struct {
+ AssertionID string
+}
+
+func (e ErrAssertionAlreadyUsed) Error() string {
+ return fmt.Sprintf("assertion ID %q has already been used (replay attack)", e.AssertionID)
+}
+
+func (s *SAMLAssertion) CheckExists(
+ ctx context.Context,
+ conn pg.Conn,
+ assertionID string,
+) (bool, error) {
+ query := `
+SELECT id
+FROM auth_saml_assertions
+WHERE id = @id
+LIMIT 1
+`
+
+ rows, err := conn.Query(ctx, query, pgx.NamedArgs{"id": assertionID})
+ if err != nil {
+ return false, fmt.Errorf("cannot query saml_assertions: %w", err)
+ }
+
+ _, err = pgx.CollectOneRow(rows, pgx.RowTo[string])
+ if err == nil {
+ return true, nil
+ }
+ if err == pgx.ErrNoRows {
+ return false, nil
+ }
+
+ return false, fmt.Errorf("cannot collect saml_assertion: %w", err)
+}
+
+func (s *SAMLAssertion) Insert(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+) error {
+ query := `
+INSERT INTO auth_saml_assertions (id, tenant_id, organization_id, used_at, expires_at)
+VALUES (@id, @tenant_id, @organization_id, @used_at, @expires_at)
+`
+
+ args := pgx.NamedArgs{
+ "id": s.ID,
+ "tenant_id": scope.GetTenantID(),
+ "organization_id": s.OrganizationID,
+ "used_at": s.UsedAt,
+ "expires_at": s.ExpiresAt,
+ }
+
+ _, err := conn.Exec(ctx, query, args)
+ if err != nil {
+ return fmt.Errorf("cannot insert saml_assertion: %w", err)
+ }
+
+ return nil
+}
+
+func DeleteExpiredSAMLAssertions(ctx context.Context, conn pg.Conn, now time.Time) (int64, error) {
+ query := `
+DELETE FROM auth_saml_assertions
+WHERE expires_at < @now
+`
+
+ result, err := conn.Exec(ctx, query, pgx.NamedArgs{"now": now})
+ if err != nil {
+ return 0, fmt.Errorf("cannot delete expired saml_assertions: %w", err)
+ }
+
+ return result.RowsAffected(), nil
+}
diff --git a/pkg/coredata/saml_configuration.go b/pkg/coredata/saml_configuration.go
new file mode 100644
index 000000000..8143c1318
--- /dev/null
+++ b/pkg/coredata/saml_configuration.go
@@ -0,0 +1,453 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 coredata
+
+import (
+ "context"
+ "fmt"
+ "maps"
+ "time"
+
+ "github.com/getprobo/probo/pkg/gid"
+ "github.com/jackc/pgx/v5"
+ "go.gearno.de/kit/pg"
+)
+
+type SAMLConfiguration struct {
+ ID gid.GID `db:"id"`
+ OrganizationID gid.GID `db:"organization_id"`
+ EmailDomain string `db:"email_domain"`
+ Enabled bool `db:"enabled"`
+ EnforcementPolicy SAMLEnforcementPolicy `db:"enforcement_policy"`
+ IdPEntityID string `db:"idp_entity_id"`
+ IdPSsoURL string `db:"idp_sso_url"`
+ IdPCertificate string `db:"idp_certificate"`
+ IdPMetadataURL *string `db:"idp_metadata_url"`
+ AttributeEmail string `db:"attribute_email"`
+ AttributeFirstname string `db:"attribute_firstname"`
+ AttributeLastname string `db:"attribute_lastname"`
+ AttributeRole string `db:"attribute_role"`
+ DefaultRole string `db:"default_role"`
+ AutoSignupEnabled bool `db:"auto_signup_enabled"`
+ DomainVerified bool `db:"domain_verified"`
+ DomainVerificationToken *string `db:"domain_verification_token"`
+ DomainVerifiedAt *time.Time `db:"domain_verified_at"`
+ CreatedAt time.Time `db:"created_at"`
+ UpdatedAt time.Time `db:"updated_at"`
+}
+
+func (s *SAMLConfiguration) LoadByOrganizationIDAndEmailDomain(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+ organizationID gid.GID,
+ emailDomain string,
+) error {
+ q := `
+SELECT
+ id,
+ organization_id,
+ email_domain,
+ enabled,
+ enforcement_policy,
+ idp_entity_id,
+ idp_sso_url,
+ idp_certificate,
+ idp_metadata_url,
+ attribute_email,
+ attribute_firstname,
+ attribute_lastname,
+ attribute_role,
+ default_role,
+ auto_signup_enabled,
+ domain_verified,
+ domain_verification_token,
+ domain_verified_at,
+ created_at,
+ updated_at
+FROM
+ auth_saml_configurations
+WHERE
+ %s
+ AND organization_id = @organization_id
+ AND email_domain = @email_domain
+LIMIT 1;
+`
+
+ q = fmt.Sprintf(q, scope.SQLFragment())
+
+ args := pgx.StrictNamedArgs{
+ "organization_id": organizationID,
+ "email_domain": emailDomain,
+ }
+ maps.Copy(args, scope.SQLArguments())
+
+ rows, err := conn.Query(ctx, q, args)
+ if err != nil {
+ return fmt.Errorf("cannot query auth_saml_configurations: %w", err)
+ }
+
+ config, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SAMLConfiguration])
+ if err != nil {
+ return fmt.Errorf("cannot collect saml_configuration: %w", err)
+ }
+
+ *s = config
+
+ return nil
+}
+
+func (s *SAMLConfiguration) LoadByID(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+ configID gid.GID,
+) error {
+ q := `
+SELECT
+ id,
+ organization_id,
+ email_domain,
+ enabled,
+ enforcement_policy,
+ idp_entity_id,
+ idp_sso_url,
+ idp_certificate,
+ idp_metadata_url,
+ attribute_email,
+ attribute_firstname,
+ attribute_lastname,
+ attribute_role,
+ default_role,
+ auto_signup_enabled,
+ domain_verified,
+ domain_verification_token,
+ domain_verified_at,
+ created_at,
+ updated_at
+FROM
+ auth_saml_configurations
+WHERE
+ %s
+ AND id = @id
+LIMIT 1;
+`
+
+ q = fmt.Sprintf(q, scope.SQLFragment())
+
+ args := pgx.StrictNamedArgs{"id": configID}
+ maps.Copy(args, scope.SQLArguments())
+
+ rows, err := conn.Query(ctx, q, args)
+ if err != nil {
+ return fmt.Errorf("cannot query auth_saml_configurations: %w", err)
+ }
+
+ config, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[SAMLConfiguration])
+ if err != nil {
+ return fmt.Errorf("cannot collect saml_configuration: %w", err)
+ }
+
+ *s = config
+
+ return nil
+}
+
+func (s *SAMLConfiguration) Insert(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+) error {
+ q := `
+INSERT INTO auth_saml_configurations (
+ id,
+ tenant_id,
+ organization_id,
+ email_domain,
+ enabled,
+ enforcement_policy,
+ idp_entity_id,
+ idp_sso_url,
+ idp_certificate,
+ idp_metadata_url,
+ attribute_email,
+ attribute_firstname,
+ attribute_lastname,
+ attribute_role,
+ default_role,
+ auto_signup_enabled,
+ domain_verified,
+ domain_verification_token,
+ domain_verified_at,
+ created_at,
+ updated_at
+) VALUES (
+ @id,
+ @tenant_id,
+ @organization_id,
+ @email_domain,
+ @enabled,
+ @enforcement_policy,
+ @idp_entity_id,
+ @idp_sso_url,
+ @idp_certificate,
+ @idp_metadata_url,
+ @attribute_email,
+ @attribute_firstname,
+ @attribute_lastname,
+ @attribute_role,
+ @default_role,
+ @auto_signup_enabled,
+ @domain_verified,
+ @domain_verification_token,
+ @domain_verified_at,
+ @created_at,
+ @updated_at
+)
+`
+
+ args := pgx.StrictNamedArgs{
+ "id": s.ID,
+ "tenant_id": scope.GetTenantID(),
+ "organization_id": s.OrganizationID,
+ "email_domain": s.EmailDomain,
+ "enabled": s.Enabled,
+ "enforcement_policy": s.EnforcementPolicy,
+ "idp_entity_id": s.IdPEntityID,
+ "idp_sso_url": s.IdPSsoURL,
+ "idp_certificate": s.IdPCertificate,
+ "idp_metadata_url": s.IdPMetadataURL,
+ "attribute_email": s.AttributeEmail,
+ "attribute_firstname": s.AttributeFirstname,
+ "attribute_lastname": s.AttributeLastname,
+ "attribute_role": s.AttributeRole,
+ "default_role": s.DefaultRole,
+ "auto_signup_enabled": s.AutoSignupEnabled,
+ "domain_verified": s.DomainVerified,
+ "domain_verification_token": s.DomainVerificationToken,
+ "domain_verified_at": s.DomainVerifiedAt,
+ "created_at": s.CreatedAt,
+ "updated_at": s.UpdatedAt,
+ }
+
+ _, err := conn.Exec(ctx, q, args)
+ if err != nil {
+ return fmt.Errorf("cannot insert saml_configuration: %w", err)
+ }
+
+ return nil
+}
+
+func (s *SAMLConfiguration) Update(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+) error {
+ q := `
+UPDATE auth_saml_configurations
+SET
+ enabled = @enabled,
+ enforcement_policy = @enforcement_policy,
+ idp_entity_id = @idp_entity_id,
+ idp_sso_url = @idp_sso_url,
+ idp_certificate = @idp_certificate,
+ idp_metadata_url = @idp_metadata_url,
+ attribute_email = @attribute_email,
+ attribute_firstname = @attribute_firstname,
+ attribute_lastname = @attribute_lastname,
+ attribute_role = @attribute_role,
+ default_role = @default_role,
+ auto_signup_enabled = @auto_signup_enabled,
+ domain_verified = @domain_verified,
+ domain_verification_token = @domain_verification_token,
+ domain_verified_at = @domain_verified_at,
+ updated_at = @updated_at
+WHERE
+ %s
+ AND id = @id
+`
+
+ q = fmt.Sprintf(q, scope.SQLFragment())
+
+ args := pgx.StrictNamedArgs{
+ "id": s.ID,
+ "enabled": s.Enabled,
+ "enforcement_policy": s.EnforcementPolicy,
+ "idp_entity_id": s.IdPEntityID,
+ "idp_sso_url": s.IdPSsoURL,
+ "idp_certificate": s.IdPCertificate,
+ "idp_metadata_url": s.IdPMetadataURL,
+ "attribute_email": s.AttributeEmail,
+ "attribute_firstname": s.AttributeFirstname,
+ "attribute_lastname": s.AttributeLastname,
+ "attribute_role": s.AttributeRole,
+ "default_role": s.DefaultRole,
+ "auto_signup_enabled": s.AutoSignupEnabled,
+ "domain_verified": s.DomainVerified,
+ "domain_verification_token": s.DomainVerificationToken,
+ "domain_verified_at": s.DomainVerifiedAt,
+ "updated_at": s.UpdatedAt,
+ }
+
+ maps.Copy(args, scope.SQLArguments())
+
+ _, err := conn.Exec(ctx, q, args)
+ if err != nil {
+ return fmt.Errorf("cannot update saml_configuration: %w", err)
+ }
+
+ return nil
+}
+
+func (s *SAMLConfiguration) Delete(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+) error {
+ q := `
+DELETE FROM auth_saml_configurations
+WHERE
+ %s
+ AND id = @id
+`
+
+ q = fmt.Sprintf(q, scope.SQLFragment())
+
+ args := pgx.StrictNamedArgs{"id": s.ID}
+ maps.Copy(args, scope.SQLArguments())
+
+ _, err := conn.Exec(ctx, q, args)
+ if err != nil {
+ return fmt.Errorf("cannot delete saml_configuration: %w", err)
+ }
+
+ return nil
+}
+
+func LoadSAMLConfigurationsByOrganizationID(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+ organizationID gid.GID,
+) ([]*SAMLConfiguration, error) {
+ q := `
+SELECT
+ id,
+ organization_id,
+ email_domain,
+ enabled,
+ enforcement_policy,
+ idp_entity_id,
+ idp_sso_url,
+ idp_certificate,
+ idp_metadata_url,
+ attribute_email,
+ attribute_firstname,
+ attribute_lastname,
+ attribute_role,
+ default_role,
+ auto_signup_enabled,
+ domain_verified,
+ domain_verification_token,
+ domain_verified_at,
+ created_at,
+ updated_at
+FROM
+ auth_saml_configurations
+WHERE
+ %s
+ AND organization_id = @organization_id
+ORDER BY email_domain ASC;
+`
+
+ q = fmt.Sprintf(q, scope.SQLFragment())
+
+ args := pgx.StrictNamedArgs{"organization_id": organizationID}
+ maps.Copy(args, scope.SQLArguments())
+
+ rows, err := conn.Query(ctx, q, args)
+ if err != nil {
+ return nil, fmt.Errorf("cannot query auth_saml_configurations: %w", err)
+ }
+
+ configs, err := pgx.CollectRows(rows, pgx.RowToStructByName[SAMLConfiguration])
+ if err != nil {
+ return nil, fmt.Errorf("cannot collect saml_configurations: %w", err)
+ }
+
+ result := make([]*SAMLConfiguration, len(configs))
+ for i := range configs {
+ result[i] = &configs[i]
+ }
+
+ return result, nil
+}
+
+// LoadAllEnabledSAMLConfigurationsByEmailDomain loads all enabled SAML configurations for a given email domain
+// This is used for SSO login detection when multiple organizations may have SAML configured for the same domain
+func LoadAllEnabledSAMLConfigurationsByEmailDomain(
+ ctx context.Context,
+ conn pg.Conn,
+ emailDomain string,
+) ([]*SAMLConfiguration, error) {
+ q := `
+SELECT
+ id,
+ organization_id,
+ email_domain,
+ enabled,
+ enforcement_policy,
+ idp_entity_id,
+ idp_sso_url,
+ idp_certificate,
+ idp_metadata_url,
+ attribute_email,
+ attribute_firstname,
+ attribute_lastname,
+ attribute_role,
+ default_role,
+ auto_signup_enabled,
+ domain_verified,
+ domain_verification_token,
+ domain_verified_at,
+ created_at,
+ updated_at
+FROM
+ auth_saml_configurations
+WHERE
+ email_domain = $1
+ AND enabled = true
+ AND domain_verified = true
+ORDER BY created_at ASC;
+`
+
+ rows, err := conn.Query(ctx, q, emailDomain)
+ if err != nil {
+ return nil, fmt.Errorf("cannot query auth_saml_configurations: %w", err)
+ }
+
+ configs, err := pgx.CollectRows(rows, pgx.RowToStructByName[SAMLConfiguration])
+ if err != nil {
+ return nil, fmt.Errorf("cannot collect saml_configurations: %w", err)
+ }
+
+ result := make([]*SAMLConfiguration, len(configs))
+ for i := range configs {
+ result[i] = &configs[i]
+ }
+
+ return result, nil
+}
+
diff --git a/pkg/coredata/saml_enforcement_policy.go b/pkg/coredata/saml_enforcement_policy.go
new file mode 100644
index 000000000..9d4e53da5
--- /dev/null
+++ b/pkg/coredata/saml_enforcement_policy.go
@@ -0,0 +1,60 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 coredata
+
+import (
+ "database/sql/driver"
+ "fmt"
+)
+
+type SAMLEnforcementPolicy string
+
+const (
+ SAMLEnforcementPolicyOff SAMLEnforcementPolicy = "OFF"
+ SAMLEnforcementPolicyOptional SAMLEnforcementPolicy = "OPTIONAL"
+ SAMLEnforcementPolicyRequired SAMLEnforcementPolicy = "REQUIRED"
+)
+
+func (sep SAMLEnforcementPolicy) String() string {
+ return string(sep)
+}
+
+func (sep *SAMLEnforcementPolicy) Scan(value any) error {
+ var s string
+ switch v := value.(type) {
+ case string:
+ s = v
+ case []byte:
+ s = string(v)
+ default:
+ return fmt.Errorf("unsupported type for SAMLEnforcementPolicy: %T", value)
+ }
+
+ switch s {
+ case "OFF":
+ *sep = SAMLEnforcementPolicyOff
+ case "OPTIONAL":
+ *sep = SAMLEnforcementPolicyOptional
+ case "REQUIRED":
+ *sep = SAMLEnforcementPolicyRequired
+ default:
+ return fmt.Errorf("invalid SAMLEnforcementPolicy value: %q", s)
+ }
+ return nil
+}
+
+func (sep SAMLEnforcementPolicy) Value() (driver.Value, error) {
+ return sep.String(), nil
+}
diff --git a/pkg/coredata/saml_relay_state.go b/pkg/coredata/saml_relay_state.go
new file mode 100644
index 000000000..9a0a41876
--- /dev/null
+++ b/pkg/coredata/saml_relay_state.go
@@ -0,0 +1,156 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 coredata
+
+import (
+ "context"
+ "crypto/rand"
+ "encoding/base64"
+ "fmt"
+ "time"
+
+ "github.com/getprobo/probo/pkg/gid"
+ "github.com/jackc/pgx/v5"
+ "go.gearno.de/kit/pg"
+)
+
+type SAMLRelayState struct {
+ Token string `db:"token"`
+ OrganizationID gid.GID `db:"organization_id"`
+ SAMLConfigID gid.GID `db:"saml_config_id"`
+ RequestID string `db:"request_id"`
+ CreatedAt time.Time `db:"created_at"`
+ ExpiresAt time.Time `db:"expires_at"`
+}
+
+type ErrRelayStateNotFound struct {
+ Token string
+}
+
+func (e ErrRelayStateNotFound) Error() string {
+ return "relay state token not found or invalid"
+}
+
+type ErrRelayStateExpired struct {
+ Token string
+ ExpiresAt time.Time
+}
+
+func (e ErrRelayStateExpired) Error() string {
+ return fmt.Sprintf("relay state token expired at %v", e.ExpiresAt)
+}
+
+func GenerateSecureToken() (string, error) {
+ b := make([]byte, 32)
+ _, err := rand.Read(b)
+ if err != nil {
+ return "", fmt.Errorf("cannot generate random token: %w", err)
+ }
+
+ token := base64.URLEncoding.EncodeToString(b)
+ return token, nil
+}
+
+func (s *SAMLRelayState) Insert(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+) error {
+ query := `
+INSERT INTO auth_saml_relay_states (token, tenant_id, organization_id, saml_config_id, request_id, created_at, expires_at)
+VALUES (@token, @tenant_id, @organization_id, @saml_config_id, @request_id, @created_at, @expires_at)
+`
+
+ args := pgx.NamedArgs{
+ "token": s.Token,
+ "tenant_id": scope.GetTenantID(),
+ "organization_id": s.OrganizationID,
+ "saml_config_id": s.SAMLConfigID,
+ "request_id": s.RequestID,
+ "created_at": s.CreatedAt,
+ "expires_at": s.ExpiresAt,
+ }
+
+ _, err := conn.Exec(ctx, query, args)
+ if err != nil {
+ return fmt.Errorf("cannot insert saml_relay_state: %w", err)
+ }
+
+ return nil
+}
+
+func (s *SAMLRelayState) Load(
+ ctx context.Context,
+ conn pg.Conn,
+ token string,
+) error {
+ query := `
+SELECT token, organization_id, saml_config_id, request_id, created_at, expires_at
+FROM auth_saml_relay_states
+WHERE token = @token
+LIMIT 1
+`
+
+ rows, err := conn.Query(ctx, query, pgx.NamedArgs{"token": token})
+ if err != nil {
+ return fmt.Errorf("cannot query saml_relay_states: %w", err)
+ }
+
+ state, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[SAMLRelayState])
+ if err == pgx.ErrNoRows {
+ return ErrRelayStateNotFound{Token: token}
+ }
+ if err != nil {
+ return fmt.Errorf("cannot collect saml_relay_state: %w", err)
+ }
+
+ *s = state
+ return nil
+}
+
+func (s *SAMLRelayState) IsExpired(now time.Time) bool {
+ return now.After(s.ExpiresAt) || now.Equal(s.ExpiresAt)
+}
+
+func (s *SAMLRelayState) Delete(
+ ctx context.Context,
+ conn pg.Conn,
+) error {
+ query := `
+DELETE FROM auth_saml_relay_states
+WHERE token = @token
+`
+
+ _, err := conn.Exec(ctx, query, pgx.NamedArgs{"token": s.Token})
+ if err != nil {
+ return fmt.Errorf("cannot delete saml_relay_state: %w", err)
+ }
+
+ return nil
+}
+
+func DeleteExpiredSAMLRelayStates(ctx context.Context, conn pg.Conn, now time.Time) (int64, error) {
+ query := `
+DELETE FROM auth_saml_relay_states
+WHERE expires_at < @now
+`
+
+ result, err := conn.Exec(ctx, query, pgx.NamedArgs{"now": now})
+ if err != nil {
+ return 0, fmt.Errorf("cannot delete expired saml_relay_states: %w", err)
+ }
+
+ return result.RowsAffected(), nil
+}
diff --git a/pkg/coredata/saml_request.go b/pkg/coredata/saml_request.go
new file mode 100644
index 000000000..faa0764a3
--- /dev/null
+++ b/pkg/coredata/saml_request.go
@@ -0,0 +1,145 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 coredata
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/getprobo/probo/pkg/gid"
+ "github.com/jackc/pgx/v5"
+ "go.gearno.de/kit/pg"
+)
+
+type SAMLRequest struct {
+ ID string `db:"id"`
+ OrganizationID gid.GID `db:"organization_id"`
+ CreatedAt time.Time `db:"created_at"`
+ ExpiresAt time.Time `db:"expires_at"`
+}
+
+type ErrSAMLRequestNotFound struct {
+ RequestID string
+}
+
+func (e ErrSAMLRequestNotFound) Error() string {
+ return fmt.Sprintf("SAML request ID %q not found", e.RequestID)
+}
+
+type ErrSAMLRequestExpired struct {
+ RequestID string
+ ExpiresAt time.Time
+}
+
+func (e ErrSAMLRequestExpired) Error() string {
+ return fmt.Sprintf("SAML request ID %q expired at %v", e.RequestID, e.ExpiresAt)
+}
+
+func (s *SAMLRequest) Insert(
+ ctx context.Context,
+ conn pg.Conn,
+ scope Scoper,
+) error {
+ query := `
+INSERT INTO auth_saml_requests (id, organization_id, tenant_id, created_at, expires_at)
+VALUES (@id, @organization_id, @tenant_id, @created_at, @expires_at)
+`
+
+ args := pgx.NamedArgs{
+ "id": s.ID,
+ "organization_id": s.OrganizationID,
+ "tenant_id": scope.GetTenantID(),
+ "created_at": s.CreatedAt,
+ "expires_at": s.ExpiresAt,
+ }
+
+ _, err := conn.Exec(ctx, query, args)
+ if err != nil {
+ return fmt.Errorf("cannot insert saml_request: %w", err)
+ }
+
+ return nil
+}
+
+func (s *SAMLRequest) Load(
+ ctx context.Context,
+ conn pg.Conn,
+ requestID string,
+ organizationID gid.GID,
+) error {
+ query := `
+SELECT id, organization_id, created_at, expires_at
+FROM auth_saml_requests
+WHERE id = @id AND organization_id = @organization_id
+LIMIT 1
+`
+
+ args := pgx.NamedArgs{
+ "id": requestID,
+ "organization_id": organizationID,
+ }
+
+ rows, err := conn.Query(ctx, query, args)
+ if err != nil {
+ return fmt.Errorf("cannot query saml_requests: %w", err)
+ }
+
+ req, err := pgx.CollectOneRow(rows, pgx.RowToStructByName[SAMLRequest])
+ if err == pgx.ErrNoRows {
+ return ErrSAMLRequestNotFound{RequestID: requestID}
+ }
+ if err != nil {
+ return fmt.Errorf("cannot collect saml_request: %w", err)
+ }
+
+ *s = req
+ return nil
+}
+
+func (s *SAMLRequest) IsExpired(now time.Time) bool {
+ return now.After(s.ExpiresAt) || now.Equal(s.ExpiresAt)
+}
+
+func (s *SAMLRequest) Delete(
+ ctx context.Context,
+ conn pg.Conn,
+) error {
+ query := `
+DELETE FROM auth_saml_requests
+WHERE id = @id
+`
+
+ _, err := conn.Exec(ctx, query, pgx.NamedArgs{"id": s.ID})
+ if err != nil {
+ return fmt.Errorf("cannot delete saml_request: %w", err)
+ }
+
+ return nil
+}
+
+func DeleteExpiredSAMLRequests(ctx context.Context, conn pg.Conn, now time.Time) (int64, error) {
+ query := `
+DELETE FROM auth_saml_requests
+WHERE expires_at < @now
+`
+
+ result, err := conn.Exec(ctx, query, pgx.NamedArgs{"now": now})
+ if err != nil {
+ return 0, fmt.Errorf("cannot delete expired saml_requests: %w", err)
+ }
+
+ return result.RowsAffected(), nil
+}
diff --git a/pkg/coredata/session.go b/pkg/coredata/session.go
index c3d052f5c..36e03ef04 100644
--- a/pkg/coredata/session.go
+++ b/pkg/coredata/session.go
@@ -35,7 +35,30 @@ type (
UpdatedAt time.Time `db:"updated_at"`
}
- SessionData struct{}
+ // SessionData stores authentication context for a user session
+ // Stored as JSONB in database
+ SessionData struct {
+ // PasswordAuthenticated indicates if user authenticated with email/password
+ // Required for accessing organizations without SAML
+ PasswordAuthenticated bool `json:"password_authenticated"`
+
+ // SAMLAuthenticatedOrgs tracks which organizations user has SAML-authenticated for
+ // Key: organization ID as string, Value: SAML authentication info
+ // Required for accessing organizations with SAML enforcement
+ SAMLAuthenticatedOrgs map[string]SAMLAuthInfo `json:"saml_authenticated_orgs,omitempty"`
+ }
+
+ // SAMLAuthInfo stores SAML authentication details for an organization
+ SAMLAuthInfo struct {
+ // AuthenticatedAt is when the user SAML-
+ AuthenticatedAt time.Time `json:"authenticated_at"`
+
+ // SAMLConfigID is the SAML configuration used for authentication
+ SAMLConfigID gid.GID `json:"saml_config_id"`
+
+ // SAMLSubject is the NameID from the SAML assertion (email address)
+ SAMLSubject string `json:"saml_subject"`
+ }
)
func (s Session) CursorKey(orderBy SessionOrderField) page.CursorKey {
@@ -47,7 +70,6 @@ func (s Session) CursorKey(orderBy SessionOrderField) page.CursorKey {
panic(fmt.Sprintf("unsupported order by: %s", orderBy))
}
-// Tenant id scope is not applied because we want to access sessions across all tenants for authentication purposes.
func (s *Session) LoadByID(
ctx context.Context,
conn pg.Conn,
diff --git a/pkg/coredata/user.go b/pkg/coredata/user.go
index 787723df0..f8ae61d7e 100644
--- a/pkg/coredata/user.go
+++ b/pkg/coredata/user.go
@@ -36,6 +36,7 @@ type (
HashedPassword []byte `db:"hashed_password"`
FullName string `db:"fullname"`
EmailAddressVerified bool `db:"email_address_verified"`
+ SAMLSubject *string `db:"saml_subject"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
@@ -158,6 +159,7 @@ SELECT
hashed_password,
email_address_verified,
fullname,
+ saml_subject,
created_at,
updated_at
FROM
@@ -201,6 +203,7 @@ SELECT
hashed_password,
email_address_verified,
fullname,
+ saml_subject,
created_at,
updated_at
FROM
@@ -234,16 +237,18 @@ LIMIT 1;
func (u *User) Insert(
ctx context.Context,
conn pg.Conn,
+ scope Scoper,
) error {
q := `
INSERT INTO
- users (id, email_address, hashed_password, email_address_verified, fullname, created_at, updated_at)
+ users (id, email_address, hashed_password, email_address_verified, fullname, saml_subject, created_at, updated_at)
VALUES (
@user_id,
@email_address,
@hashed_password,
@email_address_verified,
@fullname,
+ @saml_subject,
@created_at,
@updated_at
)
@@ -254,6 +259,7 @@ VALUES (
"email_address": u.EmailAddress,
"hashed_password": u.HashedPassword,
"fullname": u.FullName,
+ "saml_subject": u.SAMLSubject,
"created_at": u.CreatedAt,
"updated_at": u.UpdatedAt,
"email_address_verified": u.EmailAddressVerified,
@@ -341,3 +347,107 @@ WHERE
return nil
}
+
+func (u *User) Update(ctx context.Context, conn pg.Conn) error {
+ q := `
+UPDATE
+ users
+SET
+ email_address = @email_address,
+ email_address_verified = @email_address_verified,
+ saml_subject = @saml_subject,
+ updated_at = @updated_at
+WHERE
+ id = @user_id
+`
+
+ args := pgx.StrictNamedArgs{
+ "user_id": u.ID,
+ "email_address": u.EmailAddress,
+ "email_address_verified": u.EmailAddressVerified,
+ "saml_subject": u.SAMLSubject,
+ "updated_at": u.UpdatedAt,
+ }
+
+ _, err := conn.Exec(ctx, q, args)
+ if err != nil {
+ return fmt.Errorf("cannot update user: %w", err)
+ }
+
+ return nil
+}
+
+// LoadBySAMLSubject loads a user by their SAML subject (NameID)
+func (u *User) LoadBySAMLSubject(
+ ctx context.Context,
+ conn pg.Conn,
+ samlSubject string,
+) error {
+ q := `
+SELECT
+ id,
+ email_address,
+ hashed_password,
+ email_address_verified,
+ fullname,
+ saml_subject,
+ created_at,
+ updated_at
+FROM
+ users
+WHERE
+ saml_subject = @saml_subject
+LIMIT 1;
+`
+
+ args := pgx.StrictNamedArgs{"saml_subject": samlSubject}
+
+ rows, err := conn.Query(ctx, q, args)
+ if err != nil {
+ return fmt.Errorf("cannot query user by SAML subject: %w", err)
+ }
+
+ user, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[User])
+ if err != nil {
+ if errors.Is(err, pgx.ErrNoRows) {
+ return &ErrUserNotFound{Identifier: samlSubject}
+ }
+
+ return fmt.Errorf("cannot collect user: %w", err)
+ }
+
+ *u = user
+
+ return nil
+}
+
+// LoadByEmailAndTenant, LoadByEmailGlobal, and IsTenantUser methods removed
+// All users are now global (no tenant_id distinction)
+// Use LoadByEmail() for all email-based lookups
+
+func (u *User) CountMemberships(
+ ctx context.Context,
+ conn pg.Conn,
+) (int, error) {
+ q := `
+SELECT
+ COUNT(*)
+FROM
+ authz_memberships
+WHERE
+ user_id = @user_id
+`
+
+ args := pgx.StrictNamedArgs{"user_id": u.ID}
+
+ var count int
+ err := conn.QueryRow(ctx, q, args).Scan(&count)
+ if err != nil {
+ return 0, fmt.Errorf("cannot count user memberships: %w", err)
+ }
+
+ return count, nil
+}
+
+// ConvertToTenantUser method removed
+// All users are now global (no tenant conversion needed)
diff --git a/pkg/coredata/user_auth_method.go b/pkg/coredata/user_auth_method.go
new file mode 100644
index 000000000..4d0adf1d1
--- /dev/null
+++ b/pkg/coredata/user_auth_method.go
@@ -0,0 +1,22 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 coredata
+
+type UserAuthMethod string
+
+const (
+ UserAuthMethodPassword UserAuthMethod = "PASSWORD"
+ UserAuthMethodSAML UserAuthMethod = "SAML"
+)
diff --git a/pkg/probod/auth_config.go b/pkg/probod/auth_config.go
index a66ccea67..045bbca85 100644
--- a/pkg/probod/auth_config.go
+++ b/pkg/probod/auth_config.go
@@ -25,6 +25,7 @@ type (
Password passwordConfig `json:"password"`
DisableSignup bool `json:"disable-signup"`
InvitationConfirmationTokenValidity int `json:"invitation-confirmation-token-validity"`
+ SAML samlConfig `json:"saml"`
}
trustAuthConfig struct {
diff --git a/pkg/probod/probod.go b/pkg/probod/probod.go
index a4822a261..11960add6 100644
--- a/pkg/probod/probod.go
+++ b/pkg/probod/probod.go
@@ -118,6 +118,10 @@ func New() *Implm {
},
DisableSignup: false,
InvitationConfirmationTokenValidity: 3600,
+ SAML: samlConfig{
+ SessionDuration: 604800,
+ CleanupIntervalSeconds: 86400,
+ },
},
TrustAuth: trustAuthConfig{
CookieName: "TCT",
@@ -268,9 +272,11 @@ func (impl *Implm) Run(
authService, err := auth.NewService(
ctx,
pgClient,
+ impl.cfg.EncryptionKey,
hp,
impl.cfg.Auth.Cookie.Secret,
impl.cfg.Hostname,
+ fmt.Sprintf("https://%s", impl.cfg.Hostname),
impl.cfg.Auth.DisableSignup,
time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second,
)
@@ -291,6 +297,21 @@ func (impl *Implm) Run(
fileManagerService := filemanager.NewService(s3Client)
+ samlService, err := auth.NewSAMLService(
+ pgClient,
+ impl.cfg.EncryptionKey,
+ fmt.Sprintf("https://%s", impl.cfg.Hostname),
+ impl.cfg.Auth.SAML.SessionDurationTime(),
+ impl.cfg.Auth.Cookie.Name,
+ impl.cfg.Auth.Cookie.Secret,
+ impl.cfg.Auth.SAML.Certificate,
+ impl.cfg.Auth.SAML.PrivateKey,
+ l.Named("saml"),
+ )
+ if err != nil {
+ return fmt.Errorf("cannot create SAML service: %w", err)
+ }
+
var accountKey crypto.Signer
if impl.cfg.CustomDomains.ACME.AccountKey != "" {
accountKey, err = pem.DecodePrivateKey([]byte(impl.cfg.CustomDomains.ACME.AccountKey))
@@ -368,10 +389,13 @@ func (impl *Implm) Run(
Auth: authService,
Authz: authzService,
Trust: trustService,
+ SAML: samlService,
ConnectorRegistry: defaultConnectorRegistry,
Agent: agent,
SafeRedirect: &saferedirect.SafeRedirect{AllowedHost: impl.cfg.Hostname},
CustomDomainCname: impl.cfg.CustomDomains.CnameTarget,
+ FileManager: fileManagerService,
+ PGClient: pgClient,
Logger: l.Named("http.server"),
ConsoleAuth: api.ConsoleAuthConfig{
CookieName: impl.cfg.Auth.Cookie.Name,
@@ -445,6 +469,20 @@ func (impl *Implm) Run(
},
)
+ samlCleanerCtx, stopSAMLCleaner := context.WithCancel(context.Background())
+ samlCleaner := auth.NewCleaner(
+ pgClient,
+ impl.cfg.Auth.SAML.CleanupInterval(),
+ l.Named("saml-cleaner"),
+ )
+ wg.Go(
+ func() {
+ if err := samlCleaner.Run(samlCleanerCtx); err != nil {
+ cancel(fmt.Errorf("saml cleaner crashed: %w", err))
+ }
+ },
+ )
+
trustCenterServerCtx, stopTrustCenterServer := context.WithCancel(context.Background())
defer stopTrustCenterServer()
wg.Go(
@@ -460,6 +498,7 @@ func (impl *Implm) Run(
stopMailer()
stopSlackSender()
stopExportJobExporter()
+ stopSAMLCleaner()
stopApiServer()
stopTrustCenterServer()
diff --git a/pkg/probod/saml_config.go b/pkg/probod/saml_config.go
new file mode 100644
index 000000000..323504bb3
--- /dev/null
+++ b/pkg/probod/saml_config.go
@@ -0,0 +1,40 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 probod
+
+import (
+ "time"
+)
+
+type samlConfig struct {
+ SessionDuration int `json:"session-duration"`
+ CleanupIntervalSeconds int `json:"cleanup-interval-seconds"`
+ Certificate string `json:"certificate"`
+ PrivateKey string `json:"private-key"`
+}
+
+func (c samlConfig) SessionDurationTime() time.Duration {
+ if c.SessionDuration == 0 {
+ return 7 * 24 * time.Hour
+ }
+ return time.Duration(c.SessionDuration) * time.Second
+}
+
+func (c samlConfig) CleanupInterval() time.Duration {
+ if c.CleanupIntervalSeconds == 0 {
+ return 0
+ }
+ return time.Duration(c.CleanupIntervalSeconds) * time.Second
+}
diff --git a/pkg/securecookie/securecookie.go b/pkg/securecookie/securecookie.go
index c9ae79f46..9273ebb44 100644
--- a/pkg/securecookie/securecookie.go
+++ b/pkg/securecookie/securecookie.go
@@ -67,7 +67,7 @@ func DefaultConfig(name, secret string) Config {
MaxAge: 86400 * 30, // 30 days
Secure: true,
HTTPOnly: true,
- SameSite: http.SameSiteStrictMode,
+ SameSite: http.SameSiteNoneMode, // None mode required for SAML (cross-site POST from IdP)
}
}
diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go
index 633a7dd09..a23bf1d7e 100644
--- a/pkg/server/api/api.go
+++ b/pkg/server/api/api.go
@@ -59,6 +59,7 @@ type (
Auth *auth.Service
Authz *authz.Service
Trust *trust.Service
+ SAML *auth.SAMLService
ConsoleAuth ConsoleAuthConfig
TrustAuth TrustAuthConfig
ConnectorRegistry *connector.ConnectorRegistry
@@ -194,6 +195,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.cfg.ConnectorRegistry,
s.cfg.SafeRedirect,
s.cfg.CustomDomainCname,
+ s.cfg.SAML,
),
)
diff --git a/pkg/server/api/console/v1/resolver.go b/pkg/server/api/console/v1/resolver.go
index ec7c15529..00b57062d 100644
--- a/pkg/server/api/console/v1/resolver.go
+++ b/pkg/server/api/console/v1/resolver.go
@@ -60,11 +60,17 @@ type (
proboSvc *probo.Service
authSvc *auth.Service
authzSvc *authz.Service
+ samlSvc *auth.SAMLService
authCfg AuthConfig
customDomainCname string
}
ctxKey struct{ name string }
+
+ userTenantAccess struct {
+ tenantIDs []gid.TenantID
+ authErrors map[gid.TenantID]error
+ }
)
var (
@@ -92,6 +98,7 @@ func NewMux(
connectorRegistry *connector.ConnectorRegistry,
safeRedirect *saferedirect.SafeRedirect,
customDomainCname string,
+ samlSvc *auth.SAMLService,
) *chi.Mux {
r := chi.NewMux()
@@ -211,13 +218,6 @@ func NewMux(
},
)
- r.Post("/auth/register", SignUpHandler(authSvc, authCfg))
- r.Post("/auth/login", SignInHandler(authSvc, authCfg))
- r.Delete("/auth/logout", SignOutHandler(authSvc, authCfg))
- r.Post("/auth/signup-from-invitation", SignupFromInvitationHandler(authSvc, authCfg))
- r.Post("/auth/forget-password", ForgetPasswordHandler(authSvc, authCfg))
- r.Post("/auth/reset-password", ResetPasswordHandler(authSvc, authCfg))
-
r.Get("/connectors/initiate", WithSession(authSvc, authzSvc, authCfg, func(w http.ResponseWriter, r *http.Request) {
provider := r.URL.Query().Get("provider")
if provider != "SLACK" {
@@ -295,12 +295,12 @@ func NewMux(
})
r.Get("/", playground.Handler("GraphQL", "/api/console/v1/query"))
- r.Post("/query", graphqlHandler(logger, proboSvc, authSvc, authzSvc, authCfg, customDomainCname))
+ r.Post("/query", graphqlHandler(logger, proboSvc, authSvc, authzSvc, samlSvc, authCfg, customDomainCname))
return r
}
-func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthConfig, customDomainCname string) http.HandlerFunc {
+func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.Service, authzSvc *authz.Service, samlSvc *auth.SAMLService, authCfg AuthConfig, customDomainCname string) http.HandlerFunc {
var mb int64 = 1 << 20
es := schema.NewExecutableSchema(
@@ -309,6 +309,7 @@ func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.S
proboSvc: proboSvc,
authSvc: authSvc,
authzSvc: authzSvc,
+ samlSvc: samlSvc,
authCfg: authCfg,
customDomainCname: customDomainCname,
},
@@ -387,7 +388,10 @@ func WithSession(authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthCon
ctx = context.WithValue(ctx, sessionContextKey, authResult.Session)
ctx = context.WithValue(ctx, userContextKey, authResult.User)
- ctx = context.WithValue(ctx, userTenantContextKey, &authResult.TenantIDs)
+ ctx = context.WithValue(ctx, userTenantContextKey, &userTenantAccess{
+ tenantIDs: authResult.TenantIDs,
+ authErrors: authResult.AuthErrors,
+ })
next(w, r.WithContext(ctx))
@@ -425,13 +429,19 @@ func GetTenantAuthzService(ctx context.Context, authzSvc *authz.Service, tenantI
}
func validateTenantAccess(ctx context.Context, tenantID gid.TenantID) {
- tenantIDs, _ := ctx.Value(userTenantContextKey).(*[]gid.TenantID)
+ access, _ := ctx.Value(userTenantContextKey).(*userTenantAccess)
- if tenantIDs == nil {
+ if access == nil {
panic(fmt.Errorf("tenant not found"))
}
- if !slices.Contains(*tenantIDs, tenantID) {
- panic(fmt.Errorf("tenant not found"))
+ if !slices.Contains(access.tenantIDs, tenantID) {
+ if access.authErrors != nil {
+ if authErr := access.authErrors[tenantID]; authErr != nil {
+ panic(authErr)
+ }
+ }
+
+ panic(fmt.Errorf("access denied to tenant"))
}
}
diff --git a/pkg/server/api/console/v1/schema.graphql b/pkg/server/api/console/v1/schema.graphql
index ad0b62fc1..b8448ccfd 100644
--- a/pkg/server/api/console/v1/schema.graphql
+++ b/pkg/server/api/console/v1/schema.graphql
@@ -162,6 +162,34 @@ enum AuditState
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.AuditStateOutdated")
}
+enum SAMLEnforcementPolicy
+ @goModel(
+ model: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicy"
+ ) {
+ OFF
+ @goEnum(
+ value: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicyOff"
+ )
+ OPTIONAL
+ @goEnum(
+ value: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicyOptional"
+ )
+ REQUIRED
+ @goEnum(
+ value: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicyRequired"
+ )
+}
+
+enum UserAuthMethod
+ @goModel(model: "github.com/getprobo/probo/pkg/coredata.UserAuthMethod") {
+ PASSWORD
+ @goEnum(
+ value: "github.com/getprobo/probo/pkg/coredata.UserAuthMethodPassword"
+ )
+ SAML
+ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.UserAuthMethodSAML")
+}
+
enum TrustCenterVisibility
@goModel(
model: "github.com/getprobo/probo/pkg/coredata.TrustCenterVisibility"
@@ -1791,6 +1819,8 @@ type Organization implements Node {
customDomain: CustomDomain @goField(forceResolver: true)
+ samlConfigurations: [SAMLConfiguration!]! @goField(forceResolver: true)
+
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -1810,6 +1840,7 @@ type Membership implements Node {
role: String!
fullName: String!
emailAddress: String!
+ authMethod: UserAuthMethod! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -2693,7 +2724,6 @@ type VendorServiceEdge {
node: VendorService!
}
-
type VendorRiskAssessmentConnection {
edges: [VendorRiskAssessmentEdge!]!
pageInfo: PageInfo!
@@ -3174,6 +3204,28 @@ type Mutation {
deleteCustomDomain(
input: DeleteCustomDomainInput!
): DeleteCustomDomainPayload!
+
+ # SAML Configuration mutations (OWNER/ADMIN only)
+ # Step 1: Initiate domain verification (creates SAML config with unverified domain)
+ initiateDomainVerification(
+ input: InitiateDomainVerificationInput!
+ ): InitiateDomainVerificationPayload!
+
+ # Step 2: Verify domain ownership via DNS TXT record
+ verifyDomain(input: VerifyDomainInput!): VerifyDomainPayload!
+
+ # Step 3: Configure SAML (only allowed after domain is verified)
+ createSAMLConfiguration(
+ input: CreateSAMLConfigurationInput!
+ ): CreateSAMLConfigurationPayload!
+ updateSAMLConfiguration(
+ input: UpdateSAMLConfigurationInput!
+ ): UpdateSAMLConfigurationPayload!
+ deleteSAMLConfiguration(
+ input: DeleteSAMLConfigurationInput!
+ ): DeleteSAMLConfigurationPayload!
+ enableSAML(input: EnableSAMLInput!): EnableSAMLPayload!
+ disableSAML(input: DisableSAMLInput!): DisableSAMLPayload!
}
# Input Types
@@ -4794,3 +4846,167 @@ type CreateCustomDomainPayload {
type DeleteCustomDomainPayload {
deletedCustomDomainId: ID!
}
+
+# ============================================
+# SAML Configuration Types
+# ============================================
+
+type SAMLConfiguration implements Node {
+ id: ID!
+ organization: Organization! @goField(forceResolver: true)
+ emailDomain: String!
+ enabled: Boolean!
+ enforcementPolicy: SAMLEnforcementPolicy!
+
+ # Domain verification (required before SAML can be configured)
+ domainVerified: Boolean!
+ domainVerificationToken: String
+ domainVerifiedAt: Datetime
+
+ # Service Provider metadata (read-only, auto-generated)
+ spEntityId: String!
+ spAcsUrl: String!
+ spMetadataUrl: String! @goField(forceResolver: true)
+
+ # Identity Provider configuration
+ idpEntityId: String!
+ idpSsoUrl: String!
+ idpCertificate: String!
+ idpMetadataUrl: String
+
+ # Attribute mapping
+ attributeEmail: String!
+ attributeFirstname: String!
+ attributeLastname: String!
+ attributeRole: String!
+
+ # Default role for users when role attribute is missing or invalid
+ defaultRole: String!
+
+ # Auto-signup
+ autoSignupEnabled: Boolean!
+
+ # Test login URL for this configuration
+ testLoginUrl: String! @goField(forceResolver: true)
+
+ createdAt: Datetime!
+ updatedAt: Datetime!
+}
+
+# ============================================
+# SAML Configuration Inputs
+# ============================================
+
+input CreateSAMLConfigurationInput {
+ organizationId: ID!
+
+ # Email domain this config applies to
+ emailDomain: String!
+
+ # Enforcement policy for this SAML configuration
+ enforcementPolicy: SAMLEnforcementPolicy!
+
+ # SP configuration (optional - auto-generated if not provided)
+ spCertificate: String
+ spPrivateKey: String
+
+ # IdP configuration - Option 1: Provide metadata XML (recommended for Google Workspace)
+ # This will automatically extract entityId, ssoUrl, and certificate from the metadata
+ idpMetadataXml: String
+
+ # IdP configuration - Option 2: Provide individual fields manually
+ # Required if idpMetadataXml is not provided
+ idpEntityId: String
+ idpSsoUrl: String
+ idpCertificate: String
+ idpMetadataUrl: String
+
+ # Attribute mapping (optional, defaults provided)
+ attributeEmail: String
+ attributeFirstname: String
+ attributeLastname: String
+ attributeRole: String
+
+ defaultRole: String
+ autoSignupEnabled: Boolean
+}
+
+input UpdateSAMLConfigurationInput {
+ id: ID!
+
+ enabled: Boolean
+ enforcementPolicy: SAMLEnforcementPolicy
+ spCertificate: String
+ spPrivateKey: String
+ idpEntityId: String
+ idpSsoUrl: String
+ idpCertificate: String
+ idpMetadataUrl: String
+ attributeEmail: String
+ attributeFirstname: String
+ attributeLastname: String
+ attributeRole: String
+ defaultRole: String
+ autoSignupEnabled: Boolean
+}
+
+# ============================================
+# Domain Verification Inputs
+# ============================================
+
+input InitiateDomainVerificationInput {
+ organizationId: ID!
+ emailDomain: String!
+}
+
+input VerifyDomainInput {
+ id: ID!
+}
+
+input DeleteSAMLConfigurationInput {
+ id: ID!
+}
+
+input EnableSAMLInput {
+ id: ID!
+}
+
+input DisableSAMLInput {
+ id: ID!
+}
+
+# ============================================
+# SAML Configuration Payloads
+# ============================================
+
+type InitiateDomainVerificationPayload {
+ samlConfiguration: SAMLConfiguration!
+ # The TXT record value that needs to be added to DNS
+ # Format: probo-verification={token}
+ dnsRecord: String!
+}
+
+type VerifyDomainPayload {
+ samlConfiguration: SAMLConfiguration!
+ verified: Boolean!
+}
+
+type CreateSAMLConfigurationPayload {
+ samlConfiguration: SAMLConfiguration!
+}
+
+type UpdateSAMLConfigurationPayload {
+ samlConfiguration: SAMLConfiguration!
+}
+
+type DeleteSAMLConfigurationPayload {
+ deletedSAMLConfigurationId: ID!
+}
+
+type EnableSAMLPayload {
+ samlConfiguration: SAMLConfiguration!
+}
+
+type DisableSAMLPayload {
+ samlConfiguration: SAMLConfiguration!
+}
diff --git a/pkg/server/api/console/v1/schema/schema.go b/pkg/server/api/console/v1/schema/schema.go
index 6f040435d..b67bab5b5 100644
--- a/pkg/server/api/console/v1/schema/schema.go
+++ b/pkg/server/api/console/v1/schema/schema.go
@@ -68,6 +68,7 @@ type ResolverRoot interface {
InvitationConnection() InvitationConnectionResolver
Measure() MeasureResolver
MeasureConnection() MeasureConnectionResolver
+ Membership() MembershipResolver
MembershipConnection() MembershipConnectionResolver
Mutation() MutationResolver
Nonconformity() NonconformityResolver
@@ -82,6 +83,7 @@ type ResolverRoot interface {
Report() ReportResolver
Risk() RiskResolver
RiskConnection() RiskConnectionResolver
+ SAMLConfiguration() SAMLConfigurationResolver
Snapshot() SnapshotResolver
SnapshotConnection() SnapshotConnectionResolver
Task() TaskResolver
@@ -357,6 +359,10 @@ type ComplexityRoot struct {
RiskEdge func(childComplexity int) int
}
+ CreateSAMLConfigurationPayload struct {
+ SamlConfiguration func(childComplexity int) int
+ }
+
CreateSnapshotPayload struct {
SnapshotEdge func(childComplexity int) int
}
@@ -550,6 +556,10 @@ type ComplexityRoot struct {
DeletedRiskID func(childComplexity int) int
}
+ DeleteSAMLConfigurationPayload struct {
+ DeletedSAMLConfigurationID func(childComplexity int) int
+ }
+
DeleteSnapshotPayload struct {
DeletedSnapshotID func(childComplexity int) int
}
@@ -598,6 +608,10 @@ type ComplexityRoot struct {
DeletedVendorServiceID func(childComplexity int) int
}
+ DisableSAMLPayload struct {
+ SamlConfiguration func(childComplexity int) int
+ }
+
Document struct {
Classification func(childComplexity int) int
Controls func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) int
@@ -672,6 +686,10 @@ type ComplexityRoot struct {
Node func(childComplexity int) int
}
+ EnableSAMLPayload struct {
+ SamlConfiguration func(childComplexity int) int
+ }
+
Evidence struct {
CreatedAt func(childComplexity int) int
Description func(childComplexity int) int
@@ -760,6 +778,11 @@ type ComplexityRoot struct {
MeasureEdges func(childComplexity int) int
}
+ InitiateDomainVerificationPayload struct {
+ DNSRecord func(childComplexity int) int
+ SamlConfiguration func(childComplexity int) int
+ }
+
Invitation struct {
AcceptedAt func(childComplexity int) int
CreatedAt func(childComplexity int) int
@@ -813,6 +836,7 @@ type ComplexityRoot struct {
}
Membership struct {
+ AuthMethod func(childComplexity int) int
CreatedAt func(childComplexity int) int
EmailAddress func(childComplexity int) int
FullName func(childComplexity int) int
@@ -867,6 +891,7 @@ type ComplexityRoot struct {
CreateRiskDocumentMapping func(childComplexity int, input types.CreateRiskDocumentMappingInput) int
CreateRiskMeasureMapping func(childComplexity int, input types.CreateRiskMeasureMappingInput) int
CreateRiskObligationMapping func(childComplexity int, input types.CreateRiskObligationMappingInput) int
+ CreateSAMLConfiguration func(childComplexity int, input types.CreateSAMLConfigurationInput) int
CreateSnapshot func(childComplexity int, input types.CreateSnapshotInput) int
CreateTask func(childComplexity int, input types.CreateTaskInput) int
CreateTrustCenterAccess func(childComplexity int, input types.CreateTrustCenterAccessInput) int
@@ -903,6 +928,7 @@ type ComplexityRoot struct {
DeleteRiskDocumentMapping func(childComplexity int, input types.DeleteRiskDocumentMappingInput) int
DeleteRiskMeasureMapping func(childComplexity int, input types.DeleteRiskMeasureMappingInput) int
DeleteRiskObligationMapping func(childComplexity int, input types.DeleteRiskObligationMappingInput) int
+ DeleteSAMLConfiguration func(childComplexity int, input types.DeleteSAMLConfigurationInput) int
DeleteSnapshot func(childComplexity int, input types.DeleteSnapshotInput) int
DeleteTask func(childComplexity int, input types.DeleteTaskInput) int
DeleteTrustCenterAccess func(childComplexity int, input types.DeleteTrustCenterAccessInput) int
@@ -915,6 +941,8 @@ type ComplexityRoot struct {
DeleteVendorContact func(childComplexity int, input types.DeleteVendorContactInput) int
DeleteVendorDataPrivacyAgreement func(childComplexity int, input types.DeleteVendorDataPrivacyAgreementInput) int
DeleteVendorService func(childComplexity int, input types.DeleteVendorServiceInput) int
+ DisableSaml func(childComplexity int, input types.DisableSAMLInput) int
+ EnableSaml func(childComplexity int, input types.EnableSAMLInput) int
ExportDocumentVersionPDF func(childComplexity int, input types.ExportDocumentVersionPDFInput) int
ExportFramework func(childComplexity int, input types.ExportFrameworkInput) int
GenerateDocumentChangelog func(childComplexity int, input types.GenerateDocumentChangelogInput) int
@@ -922,6 +950,7 @@ type ComplexityRoot struct {
GetTrustCenterFile func(childComplexity int, input types.GetTrustCenterFileInput) int
ImportFramework func(childComplexity int, input types.ImportFrameworkInput) int
ImportMeasure func(childComplexity int, input types.ImportMeasureInput) int
+ InitiateDomainVerification func(childComplexity int, input types.InitiateDomainVerificationInput) int
InviteUser func(childComplexity int, input types.InviteUserInput) int
PublishDocumentVersion func(childComplexity int, input types.PublishDocumentVersionInput) int
RemoveMember func(childComplexity int, input types.RemoveMemberInput) int
@@ -943,6 +972,7 @@ type ComplexityRoot struct {
UpdatePeople func(childComplexity int, input types.UpdatePeopleInput) int
UpdateProcessingActivity func(childComplexity int, input types.UpdateProcessingActivityInput) int
UpdateRisk func(childComplexity int, input types.UpdateRiskInput) int
+ UpdateSAMLConfiguration func(childComplexity int, input types.UpdateSAMLConfigurationInput) int
UpdateTask func(childComplexity int, input types.UpdateTaskInput) int
UpdateTrustCenter func(childComplexity int, input types.UpdateTrustCenterInput) int
UpdateTrustCenterAccess func(childComplexity int, input types.UpdateTrustCenterAccessInput) int
@@ -959,6 +989,7 @@ type ComplexityRoot struct {
UploadVendorBusinessAssociateAgreement func(childComplexity int, input types.UploadVendorBusinessAssociateAgreementInput) int
UploadVendorComplianceReport func(childComplexity int, input types.UploadVendorComplianceReportInput) int
UploadVendorDataPrivacyAgreement func(childComplexity int, input types.UploadVendorDataPrivacyAgreementInput) int
+ VerifyDomain func(childComplexity int, input types.VerifyDomainInput) int
}
Nonconformity struct {
@@ -1044,6 +1075,7 @@ type ComplexityRoot struct {
Peoples func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.PeopleOrderBy, filter *types.PeopleFilter) int
ProcessingActivities func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ProcessingActivityOrderBy, filter *types.ProcessingActivityFilter) int
Risks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskOrderBy, filter *types.RiskFilter) int
+ SamlConfigurations func(childComplexity int) int
SlackConnections func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
Snapshots func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SnapshotOrderBy) int
Tasks func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) int
@@ -1199,6 +1231,33 @@ type ComplexityRoot struct {
Node func(childComplexity int) int
}
+ SAMLConfiguration struct {
+ AttributeEmail func(childComplexity int) int
+ AttributeFirstname func(childComplexity int) int
+ AttributeLastname func(childComplexity int) int
+ AttributeRole func(childComplexity int) int
+ AutoSignupEnabled func(childComplexity int) int
+ CreatedAt func(childComplexity int) int
+ DefaultRole func(childComplexity int) int
+ DomainVerificationToken func(childComplexity int) int
+ DomainVerified func(childComplexity int) int
+ DomainVerifiedAt func(childComplexity int) int
+ EmailDomain func(childComplexity int) int
+ Enabled func(childComplexity int) int
+ EnforcementPolicy func(childComplexity int) int
+ ID func(childComplexity int) int
+ IdpCertificate func(childComplexity int) int
+ IdpEntityID func(childComplexity int) int
+ IdpMetadataURL func(childComplexity int) int
+ IdpSsoURL func(childComplexity int) int
+ Organization func(childComplexity int) int
+ SpAcsURL func(childComplexity int) int
+ SpEntityID func(childComplexity int) int
+ SpMetadataURL func(childComplexity int) int
+ TestLoginURL func(childComplexity int) int
+ UpdatedAt func(childComplexity int) int
+ }
+
SendSigningNotificationsPayload struct {
Success func(childComplexity int) int
}
@@ -1446,6 +1505,10 @@ type ComplexityRoot struct {
Risk func(childComplexity int) int
}
+ UpdateSAMLConfigurationPayload struct {
+ SamlConfiguration func(childComplexity int) int
+ }
+
UpdateTaskPayload struct {
Task func(childComplexity int) int
}
@@ -1680,6 +1743,11 @@ type ComplexityRoot struct {
Node func(childComplexity int) int
}
+ VerifyDomainPayload struct {
+ SamlConfiguration func(childComplexity int) int
+ Verified func(childComplexity int) int
+ }
+
Viewer struct {
ID func(childComplexity int) int
Invitations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.InvitationOrder, filter *types.InvitationFilter) int
@@ -1790,6 +1858,9 @@ type MeasureResolver interface {
type MeasureConnectionResolver interface {
TotalCount(ctx context.Context, obj *types.MeasureConnection) (int, error)
}
+type MembershipResolver interface {
+ AuthMethod(ctx context.Context, obj *types.Membership) (coredata.UserAuthMethod, error)
+}
type MembershipConnectionResolver interface {
TotalCount(ctx context.Context, obj *types.MembershipConnection) (int, error)
}
@@ -1918,6 +1989,13 @@ type MutationResolver interface {
DeleteSnapshot(ctx context.Context, input types.DeleteSnapshotInput) (*types.DeleteSnapshotPayload, error)
CreateCustomDomain(ctx context.Context, input types.CreateCustomDomainInput) (*types.CreateCustomDomainPayload, error)
DeleteCustomDomain(ctx context.Context, input types.DeleteCustomDomainInput) (*types.DeleteCustomDomainPayload, error)
+ InitiateDomainVerification(ctx context.Context, input types.InitiateDomainVerificationInput) (*types.InitiateDomainVerificationPayload, error)
+ VerifyDomain(ctx context.Context, input types.VerifyDomainInput) (*types.VerifyDomainPayload, error)
+ CreateSAMLConfiguration(ctx context.Context, input types.CreateSAMLConfigurationInput) (*types.CreateSAMLConfigurationPayload, error)
+ UpdateSAMLConfiguration(ctx context.Context, input types.UpdateSAMLConfigurationInput) (*types.UpdateSAMLConfigurationPayload, error)
+ DeleteSAMLConfiguration(ctx context.Context, input types.DeleteSAMLConfigurationInput) (*types.DeleteSAMLConfigurationPayload, error)
+ EnableSaml(ctx context.Context, input types.EnableSAMLInput) (*types.EnableSAMLPayload, error)
+ DisableSaml(ctx context.Context, input types.DisableSAMLInput) (*types.DisableSAMLPayload, error)
}
type NonconformityResolver interface {
Organization(ctx context.Context, obj *types.Nonconformity) (*types.Organization, error)
@@ -1963,6 +2041,7 @@ type OrganizationResolver interface {
TrustCenterFiles(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.OrderBy[coredata.TrustCenterFileOrderField]) (*types.TrustCenterFileConnection, error)
TrustCenter(ctx context.Context, obj *types.Organization) (*types.TrustCenter, error)
CustomDomain(ctx context.Context, obj *types.Organization) (*types.CustomDomain, error)
+ SamlConfigurations(ctx context.Context, obj *types.Organization) ([]*types.SAMLConfiguration, error)
}
type PeopleConnectionResolver interface {
TotalCount(ctx context.Context, obj *types.PeopleConnection) (int, error)
@@ -1993,6 +2072,13 @@ type RiskResolver interface {
type RiskConnectionResolver interface {
TotalCount(ctx context.Context, obj *types.RiskConnection) (int, error)
}
+type SAMLConfigurationResolver interface {
+ Organization(ctx context.Context, obj *types.SAMLConfiguration) (*types.Organization, error)
+
+ SpMetadataURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error)
+
+ TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error)
+}
type SnapshotResolver interface {
Organization(ctx context.Context, obj *types.Snapshot) (*types.Organization, error)
@@ -2923,6 +3009,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.CreateRiskPayload.RiskEdge(childComplexity), true
+ case "CreateSAMLConfigurationPayload.samlConfiguration":
+ if e.complexity.CreateSAMLConfigurationPayload.SamlConfiguration == nil {
+ break
+ }
+
+ return e.complexity.CreateSAMLConfigurationPayload.SamlConfiguration(childComplexity), true
+
case "CreateSnapshotPayload.snapshotEdge":
if e.complexity.CreateSnapshotPayload.SnapshotEdge == nil {
break
@@ -3418,6 +3511,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.DeleteRiskPayload.DeletedRiskID(childComplexity), true
+ case "DeleteSAMLConfigurationPayload.deletedSAMLConfigurationId":
+ if e.complexity.DeleteSAMLConfigurationPayload.DeletedSAMLConfigurationID == nil {
+ break
+ }
+
+ return e.complexity.DeleteSAMLConfigurationPayload.DeletedSAMLConfigurationID(childComplexity), true
+
case "DeleteSnapshotPayload.deletedSnapshotId":
if e.complexity.DeleteSnapshotPayload.DeletedSnapshotID == nil {
break
@@ -3502,6 +3602,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.DeleteVendorServicePayload.DeletedVendorServiceID(childComplexity), true
+ case "DisableSAMLPayload.samlConfiguration":
+ if e.complexity.DisableSAMLPayload.SamlConfiguration == nil {
+ break
+ }
+
+ return e.complexity.DisableSAMLPayload.SamlConfiguration(childComplexity), true
+
case "Document.classification":
if e.complexity.Document.Classification == nil {
break
@@ -3846,6 +3953,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.DocumentVersionSignatureEdge.Node(childComplexity), true
+ case "EnableSAMLPayload.samlConfiguration":
+ if e.complexity.EnableSAMLPayload.SamlConfiguration == nil {
+ break
+ }
+
+ return e.complexity.EnableSAMLPayload.SamlConfiguration(childComplexity), true
+
case "Evidence.createdAt":
if e.complexity.Evidence.CreatedAt == nil {
break
@@ -4152,6 +4266,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.ImportMeasurePayload.MeasureEdges(childComplexity), true
+ case "InitiateDomainVerificationPayload.dnsRecord":
+ if e.complexity.InitiateDomainVerificationPayload.DNSRecord == nil {
+ break
+ }
+
+ return e.complexity.InitiateDomainVerificationPayload.DNSRecord(childComplexity), true
+
+ case "InitiateDomainVerificationPayload.samlConfiguration":
+ if e.complexity.InitiateDomainVerificationPayload.SamlConfiguration == nil {
+ break
+ }
+
+ return e.complexity.InitiateDomainVerificationPayload.SamlConfiguration(childComplexity), true
+
case "Invitation.acceptedAt":
if e.complexity.Invitation.AcceptedAt == nil {
break
@@ -4389,6 +4517,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.MeasureEdge.Node(childComplexity), true
+ case "Membership.authMethod":
+ if e.complexity.Membership.AuthMethod == nil {
+ break
+ }
+
+ return e.complexity.Membership.AuthMethod(childComplexity), true
+
case "Membership.createdAt":
if e.complexity.Membership.CreatedAt == nil {
break
@@ -4864,6 +4999,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Mutation.CreateRiskObligationMapping(childComplexity, args["input"].(types.CreateRiskObligationMappingInput)), true
+ case "Mutation.createSAMLConfiguration":
+ if e.complexity.Mutation.CreateSAMLConfiguration == nil {
+ break
+ }
+
+ args, err := ec.field_Mutation_createSAMLConfiguration_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Mutation.CreateSAMLConfiguration(childComplexity, args["input"].(types.CreateSAMLConfigurationInput)), true
+
case "Mutation.createSnapshot":
if e.complexity.Mutation.CreateSnapshot == nil {
break
@@ -5296,6 +5443,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Mutation.DeleteRiskObligationMapping(childComplexity, args["input"].(types.DeleteRiskObligationMappingInput)), true
+ case "Mutation.deleteSAMLConfiguration":
+ if e.complexity.Mutation.DeleteSAMLConfiguration == nil {
+ break
+ }
+
+ args, err := ec.field_Mutation_deleteSAMLConfiguration_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Mutation.DeleteSAMLConfiguration(childComplexity, args["input"].(types.DeleteSAMLConfigurationInput)), true
+
case "Mutation.deleteSnapshot":
if e.complexity.Mutation.DeleteSnapshot == nil {
break
@@ -5440,6 +5599,30 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Mutation.DeleteVendorService(childComplexity, args["input"].(types.DeleteVendorServiceInput)), true
+ case "Mutation.disableSAML":
+ if e.complexity.Mutation.DisableSaml == nil {
+ break
+ }
+
+ args, err := ec.field_Mutation_disableSAML_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Mutation.DisableSaml(childComplexity, args["input"].(types.DisableSAMLInput)), true
+
+ case "Mutation.enableSAML":
+ if e.complexity.Mutation.EnableSaml == nil {
+ break
+ }
+
+ args, err := ec.field_Mutation_enableSAML_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Mutation.EnableSaml(childComplexity, args["input"].(types.EnableSAMLInput)), true
+
case "Mutation.exportDocumentVersionPDF":
if e.complexity.Mutation.ExportDocumentVersionPDF == nil {
break
@@ -5524,6 +5707,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Mutation.ImportMeasure(childComplexity, args["input"].(types.ImportMeasureInput)), true
+ case "Mutation.initiateDomainVerification":
+ if e.complexity.Mutation.InitiateDomainVerification == nil {
+ break
+ }
+
+ args, err := ec.field_Mutation_initiateDomainVerification_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Mutation.InitiateDomainVerification(childComplexity, args["input"].(types.InitiateDomainVerificationInput)), true
+
case "Mutation.inviteUser":
if e.complexity.Mutation.InviteUser == nil {
break
@@ -5776,6 +5971,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Mutation.UpdateRisk(childComplexity, args["input"].(types.UpdateRiskInput)), true
+ case "Mutation.updateSAMLConfiguration":
+ if e.complexity.Mutation.UpdateSAMLConfiguration == nil {
+ break
+ }
+
+ args, err := ec.field_Mutation_updateSAMLConfiguration_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Mutation.UpdateSAMLConfiguration(childComplexity, args["input"].(types.UpdateSAMLConfigurationInput)), true
+
case "Mutation.updateTask":
if e.complexity.Mutation.UpdateTask == nil {
break
@@ -5968,6 +6175,18 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Mutation.UploadVendorDataPrivacyAgreement(childComplexity, args["input"].(types.UploadVendorDataPrivacyAgreementInput)), true
+ case "Mutation.verifyDomain":
+ if e.complexity.Mutation.VerifyDomain == nil {
+ break
+ }
+
+ args, err := ec.field_Mutation_verifyDomain_args(ctx, rawArgs)
+ if err != nil {
+ return 0, false
+ }
+
+ return e.complexity.Mutation.VerifyDomain(childComplexity, args["input"].(types.VerifyDomainInput)), true
+
case "Nonconformity.audit":
if e.complexity.Nonconformity.Audit == nil {
break
@@ -6491,6 +6710,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.Organization.Risks(childComplexity, args["first"].(*int), args["after"].(*page.CursorKey), args["last"].(*int), args["before"].(*page.CursorKey), args["orderBy"].(*types.RiskOrderBy), args["filter"].(*types.RiskFilter)), true
+ case "Organization.samlConfigurations":
+ if e.complexity.Organization.SamlConfigurations == nil {
+ break
+ }
+
+ return e.complexity.Organization.SamlConfigurations(childComplexity), true
+
case "Organization.slackConnections":
if e.complexity.Organization.SlackConnections == nil {
break
@@ -7234,6 +7460,174 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.RiskEdge.Node(childComplexity), true
+ case "SAMLConfiguration.attributeEmail":
+ if e.complexity.SAMLConfiguration.AttributeEmail == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.AttributeEmail(childComplexity), true
+
+ case "SAMLConfiguration.attributeFirstname":
+ if e.complexity.SAMLConfiguration.AttributeFirstname == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.AttributeFirstname(childComplexity), true
+
+ case "SAMLConfiguration.attributeLastname":
+ if e.complexity.SAMLConfiguration.AttributeLastname == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.AttributeLastname(childComplexity), true
+
+ case "SAMLConfiguration.attributeRole":
+ if e.complexity.SAMLConfiguration.AttributeRole == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.AttributeRole(childComplexity), true
+
+ case "SAMLConfiguration.autoSignupEnabled":
+ if e.complexity.SAMLConfiguration.AutoSignupEnabled == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.AutoSignupEnabled(childComplexity), true
+
+ case "SAMLConfiguration.createdAt":
+ if e.complexity.SAMLConfiguration.CreatedAt == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.CreatedAt(childComplexity), true
+
+ case "SAMLConfiguration.defaultRole":
+ if e.complexity.SAMLConfiguration.DefaultRole == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.DefaultRole(childComplexity), true
+
+ case "SAMLConfiguration.domainVerificationToken":
+ if e.complexity.SAMLConfiguration.DomainVerificationToken == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.DomainVerificationToken(childComplexity), true
+
+ case "SAMLConfiguration.domainVerified":
+ if e.complexity.SAMLConfiguration.DomainVerified == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.DomainVerified(childComplexity), true
+
+ case "SAMLConfiguration.domainVerifiedAt":
+ if e.complexity.SAMLConfiguration.DomainVerifiedAt == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.DomainVerifiedAt(childComplexity), true
+
+ case "SAMLConfiguration.emailDomain":
+ if e.complexity.SAMLConfiguration.EmailDomain == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.EmailDomain(childComplexity), true
+
+ case "SAMLConfiguration.enabled":
+ if e.complexity.SAMLConfiguration.Enabled == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.Enabled(childComplexity), true
+
+ case "SAMLConfiguration.enforcementPolicy":
+ if e.complexity.SAMLConfiguration.EnforcementPolicy == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.EnforcementPolicy(childComplexity), true
+
+ case "SAMLConfiguration.id":
+ if e.complexity.SAMLConfiguration.ID == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.ID(childComplexity), true
+
+ case "SAMLConfiguration.idpCertificate":
+ if e.complexity.SAMLConfiguration.IdpCertificate == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.IdpCertificate(childComplexity), true
+
+ case "SAMLConfiguration.idpEntityId":
+ if e.complexity.SAMLConfiguration.IdpEntityID == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.IdpEntityID(childComplexity), true
+
+ case "SAMLConfiguration.idpMetadataUrl":
+ if e.complexity.SAMLConfiguration.IdpMetadataURL == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.IdpMetadataURL(childComplexity), true
+
+ case "SAMLConfiguration.idpSsoUrl":
+ if e.complexity.SAMLConfiguration.IdpSsoURL == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.IdpSsoURL(childComplexity), true
+
+ case "SAMLConfiguration.organization":
+ if e.complexity.SAMLConfiguration.Organization == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.Organization(childComplexity), true
+
+ case "SAMLConfiguration.spAcsUrl":
+ if e.complexity.SAMLConfiguration.SpAcsURL == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.SpAcsURL(childComplexity), true
+
+ case "SAMLConfiguration.spEntityId":
+ if e.complexity.SAMLConfiguration.SpEntityID == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.SpEntityID(childComplexity), true
+
+ case "SAMLConfiguration.spMetadataUrl":
+ if e.complexity.SAMLConfiguration.SpMetadataURL == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.SpMetadataURL(childComplexity), true
+
+ case "SAMLConfiguration.testLoginUrl":
+ if e.complexity.SAMLConfiguration.TestLoginURL == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.TestLoginURL(childComplexity), true
+
+ case "SAMLConfiguration.updatedAt":
+ if e.complexity.SAMLConfiguration.UpdatedAt == nil {
+ break
+ }
+
+ return e.complexity.SAMLConfiguration.UpdatedAt(childComplexity), true
+
case "SendSigningNotificationsPayload.success":
if e.complexity.SendSigningNotificationsPayload.Success == nil {
break
@@ -8106,6 +8500,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.UpdateRiskPayload.Risk(childComplexity), true
+ case "UpdateSAMLConfigurationPayload.samlConfiguration":
+ if e.complexity.UpdateSAMLConfigurationPayload.SamlConfiguration == nil {
+ break
+ }
+
+ return e.complexity.UpdateSAMLConfigurationPayload.SamlConfiguration(childComplexity), true
+
case "UpdateTaskPayload.task":
if e.complexity.UpdateTaskPayload.Task == nil {
break
@@ -9008,6 +9409,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
return e.complexity.VendorServiceEdge.Node(childComplexity), true
+ case "VerifyDomainPayload.samlConfiguration":
+ if e.complexity.VerifyDomainPayload.SamlConfiguration == nil {
+ break
+ }
+
+ return e.complexity.VerifyDomainPayload.SamlConfiguration(childComplexity), true
+
+ case "VerifyDomainPayload.verified":
+ if e.complexity.VerifyDomainPayload.Verified == nil {
+ break
+ }
+
+ return e.complexity.VerifyDomainPayload.Verified(childComplexity), true
+
case "Viewer.id":
if e.complexity.Viewer.ID == nil {
break
@@ -9094,6 +9509,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputCreateRiskInput,
ec.unmarshalInputCreateRiskMeasureMappingInput,
ec.unmarshalInputCreateRiskObligationMappingInput,
+ ec.unmarshalInputCreateSAMLConfigurationInput,
ec.unmarshalInputCreateSnapshotInput,
ec.unmarshalInputCreateTaskInput,
ec.unmarshalInputCreateTrustCenterAccessInput,
@@ -9132,6 +9548,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputDeleteRiskInput,
ec.unmarshalInputDeleteRiskMeasureMappingInput,
ec.unmarshalInputDeleteRiskObligationMappingInput,
+ ec.unmarshalInputDeleteSAMLConfigurationInput,
ec.unmarshalInputDeleteSnapshotInput,
ec.unmarshalInputDeleteTaskInput,
ec.unmarshalInputDeleteTrustCenterAccessInput,
@@ -9144,12 +9561,14 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputDeleteVendorDataPrivacyAgreementInput,
ec.unmarshalInputDeleteVendorInput,
ec.unmarshalInputDeleteVendorServiceInput,
+ ec.unmarshalInputDisableSAMLInput,
ec.unmarshalInputDocumentFilter,
ec.unmarshalInputDocumentOrder,
ec.unmarshalInputDocumentVersionFilter,
ec.unmarshalInputDocumentVersionOrder,
ec.unmarshalInputDocumentVersionSignatureFilter,
ec.unmarshalInputDocumentVersionSignatureOrder,
+ ec.unmarshalInputEnableSAMLInput,
ec.unmarshalInputEvidenceOrder,
ec.unmarshalInputExportDocumentVersionPDFInput,
ec.unmarshalInputExportFrameworkInput,
@@ -9160,6 +9579,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputGetTrustCenterFileInput,
ec.unmarshalInputImportFrameworkInput,
ec.unmarshalInputImportMeasureInput,
+ ec.unmarshalInputInitiateDomainVerificationInput,
ec.unmarshalInputInvitationFilter,
ec.unmarshalInputInvitationOrder,
ec.unmarshalInputInviteUserInput,
@@ -9204,6 +9624,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputUpdatePeopleInput,
ec.unmarshalInputUpdateProcessingActivityInput,
ec.unmarshalInputUpdateRiskInput,
+ ec.unmarshalInputUpdateSAMLConfigurationInput,
ec.unmarshalInputUpdateTaskInput,
ec.unmarshalInputUpdateTrustCenterAccessInput,
ec.unmarshalInputUpdateTrustCenterFileInput,
@@ -9227,6 +9648,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler {
ec.unmarshalInputVendorOrder,
ec.unmarshalInputVendorRiskAssessmentOrder,
ec.unmarshalInputVendorServiceOrder,
+ ec.unmarshalInputVerifyDomainInput,
)
first := true
@@ -9488,6 +9910,34 @@ enum AuditState
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.AuditStateOutdated")
}
+enum SAMLEnforcementPolicy
+ @goModel(
+ model: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicy"
+ ) {
+ OFF
+ @goEnum(
+ value: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicyOff"
+ )
+ OPTIONAL
+ @goEnum(
+ value: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicyOptional"
+ )
+ REQUIRED
+ @goEnum(
+ value: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicyRequired"
+ )
+}
+
+enum UserAuthMethod
+ @goModel(model: "github.com/getprobo/probo/pkg/coredata.UserAuthMethod") {
+ PASSWORD
+ @goEnum(
+ value: "github.com/getprobo/probo/pkg/coredata.UserAuthMethodPassword"
+ )
+ SAML
+ @goEnum(value: "github.com/getprobo/probo/pkg/coredata.UserAuthMethodSAML")
+}
+
enum TrustCenterVisibility
@goModel(
model: "github.com/getprobo/probo/pkg/coredata.TrustCenterVisibility"
@@ -11117,6 +11567,8 @@ type Organization implements Node {
customDomain: CustomDomain @goField(forceResolver: true)
+ samlConfigurations: [SAMLConfiguration!]! @goField(forceResolver: true)
+
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -11136,6 +11588,7 @@ type Membership implements Node {
role: String!
fullName: String!
emailAddress: String!
+ authMethod: UserAuthMethod! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
@@ -12019,7 +12472,6 @@ type VendorServiceEdge {
node: VendorService!
}
-
type VendorRiskAssessmentConnection {
edges: [VendorRiskAssessmentEdge!]!
pageInfo: PageInfo!
@@ -12500,6 +12952,28 @@ type Mutation {
deleteCustomDomain(
input: DeleteCustomDomainInput!
): DeleteCustomDomainPayload!
+
+ # SAML Configuration mutations (OWNER/ADMIN only)
+ # Step 1: Initiate domain verification (creates SAML config with unverified domain)
+ initiateDomainVerification(
+ input: InitiateDomainVerificationInput!
+ ): InitiateDomainVerificationPayload!
+
+ # Step 2: Verify domain ownership via DNS TXT record
+ verifyDomain(input: VerifyDomainInput!): VerifyDomainPayload!
+
+ # Step 3: Configure SAML (only allowed after domain is verified)
+ createSAMLConfiguration(
+ input: CreateSAMLConfigurationInput!
+ ): CreateSAMLConfigurationPayload!
+ updateSAMLConfiguration(
+ input: UpdateSAMLConfigurationInput!
+ ): UpdateSAMLConfigurationPayload!
+ deleteSAMLConfiguration(
+ input: DeleteSAMLConfigurationInput!
+ ): DeleteSAMLConfigurationPayload!
+ enableSAML(input: EnableSAMLInput!): EnableSAMLPayload!
+ disableSAML(input: DisableSAMLInput!): DisableSAMLPayload!
}
# Input Types
@@ -14120,6 +14594,170 @@ type CreateCustomDomainPayload {
type DeleteCustomDomainPayload {
deletedCustomDomainId: ID!
}
+
+# ============================================
+# SAML Configuration Types
+# ============================================
+
+type SAMLConfiguration implements Node {
+ id: ID!
+ organization: Organization! @goField(forceResolver: true)
+ emailDomain: String!
+ enabled: Boolean!
+ enforcementPolicy: SAMLEnforcementPolicy!
+
+ # Domain verification (required before SAML can be configured)
+ domainVerified: Boolean!
+ domainVerificationToken: String
+ domainVerifiedAt: Datetime
+
+ # Service Provider metadata (read-only, auto-generated)
+ spEntityId: String!
+ spAcsUrl: String!
+ spMetadataUrl: String! @goField(forceResolver: true)
+
+ # Identity Provider configuration
+ idpEntityId: String!
+ idpSsoUrl: String!
+ idpCertificate: String!
+ idpMetadataUrl: String
+
+ # Attribute mapping
+ attributeEmail: String!
+ attributeFirstname: String!
+ attributeLastname: String!
+ attributeRole: String!
+
+ # Default role for users when role attribute is missing or invalid
+ defaultRole: String!
+
+ # Auto-signup
+ autoSignupEnabled: Boolean!
+
+ # Test login URL for this configuration
+ testLoginUrl: String! @goField(forceResolver: true)
+
+ createdAt: Datetime!
+ updatedAt: Datetime!
+}
+
+# ============================================
+# SAML Configuration Inputs
+# ============================================
+
+input CreateSAMLConfigurationInput {
+ organizationId: ID!
+
+ # Email domain this config applies to
+ emailDomain: String!
+
+ # Enforcement policy for this SAML configuration
+ enforcementPolicy: SAMLEnforcementPolicy!
+
+ # SP configuration (optional - auto-generated if not provided)
+ spCertificate: String
+ spPrivateKey: String
+
+ # IdP configuration - Option 1: Provide metadata XML (recommended for Google Workspace)
+ # This will automatically extract entityId, ssoUrl, and certificate from the metadata
+ idpMetadataXml: String
+
+ # IdP configuration - Option 2: Provide individual fields manually
+ # Required if idpMetadataXml is not provided
+ idpEntityId: String
+ idpSsoUrl: String
+ idpCertificate: String
+ idpMetadataUrl: String
+
+ # Attribute mapping (optional, defaults provided)
+ attributeEmail: String
+ attributeFirstname: String
+ attributeLastname: String
+ attributeRole: String
+
+ defaultRole: String
+ autoSignupEnabled: Boolean
+}
+
+input UpdateSAMLConfigurationInput {
+ id: ID!
+
+ enabled: Boolean
+ enforcementPolicy: SAMLEnforcementPolicy
+ spCertificate: String
+ spPrivateKey: String
+ idpEntityId: String
+ idpSsoUrl: String
+ idpCertificate: String
+ idpMetadataUrl: String
+ attributeEmail: String
+ attributeFirstname: String
+ attributeLastname: String
+ attributeRole: String
+ defaultRole: String
+ autoSignupEnabled: Boolean
+}
+
+# ============================================
+# Domain Verification Inputs
+# ============================================
+
+input InitiateDomainVerificationInput {
+ organizationId: ID!
+ emailDomain: String!
+}
+
+input VerifyDomainInput {
+ id: ID!
+}
+
+input DeleteSAMLConfigurationInput {
+ id: ID!
+}
+
+input EnableSAMLInput {
+ id: ID!
+}
+
+input DisableSAMLInput {
+ id: ID!
+}
+
+# ============================================
+# SAML Configuration Payloads
+# ============================================
+
+type InitiateDomainVerificationPayload {
+ samlConfiguration: SAMLConfiguration!
+ # The TXT record value that needs to be added to DNS
+ # Format: probo-verification={token}
+ dnsRecord: String!
+}
+
+type VerifyDomainPayload {
+ samlConfiguration: SAMLConfiguration!
+ verified: Boolean!
+}
+
+type CreateSAMLConfigurationPayload {
+ samlConfiguration: SAMLConfiguration!
+}
+
+type UpdateSAMLConfigurationPayload {
+ samlConfiguration: SAMLConfiguration!
+}
+
+type DeleteSAMLConfigurationPayload {
+ deletedSAMLConfigurationId: ID!
+}
+
+type EnableSAMLPayload {
+ samlConfiguration: SAMLConfiguration!
+}
+
+type DisableSAMLPayload {
+ samlConfiguration: SAMLConfiguration!
+}
`, BuiltIn: false},
}
var parsedSchema = gqlparser.MustLoadSchema(sources...)
@@ -16451,6 +17089,29 @@ func (ec *executionContext) field_Mutation_createRisk_argsInput(
return zeroVal, nil
}
+func (ec *executionContext) field_Mutation_createSAMLConfiguration_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Mutation_createSAMLConfiguration_argsInput(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["input"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_Mutation_createSAMLConfiguration_argsInput(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (types.CreateSAMLConfigurationInput, error) {
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
+ if tmp, ok := rawArgs["input"]; ok {
+ return ec.unmarshalNCreateSAMLConfigurationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateSAMLConfigurationInput(ctx, tmp)
+ }
+
+ var zeroVal types.CreateSAMLConfigurationInput
+ return zeroVal, nil
+}
+
func (ec *executionContext) field_Mutation_createSnapshot_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -17279,6 +17940,29 @@ func (ec *executionContext) field_Mutation_deleteRisk_argsInput(
return zeroVal, nil
}
+func (ec *executionContext) field_Mutation_deleteSAMLConfiguration_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Mutation_deleteSAMLConfiguration_argsInput(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["input"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_Mutation_deleteSAMLConfiguration_argsInput(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (types.DeleteSAMLConfigurationInput, error) {
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
+ if tmp, ok := rawArgs["input"]; ok {
+ return ec.unmarshalNDeleteSAMLConfigurationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteSAMLConfigurationInput(ctx, tmp)
+ }
+
+ var zeroVal types.DeleteSAMLConfigurationInput
+ return zeroVal, nil
+}
+
func (ec *executionContext) field_Mutation_deleteSnapshot_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -17555,6 +18239,52 @@ func (ec *executionContext) field_Mutation_deleteVendor_argsInput(
return zeroVal, nil
}
+func (ec *executionContext) field_Mutation_disableSAML_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Mutation_disableSAML_argsInput(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["input"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_Mutation_disableSAML_argsInput(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (types.DisableSAMLInput, error) {
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
+ if tmp, ok := rawArgs["input"]; ok {
+ return ec.unmarshalNDisableSAMLInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDisableSAMLInput(ctx, tmp)
+ }
+
+ var zeroVal types.DisableSAMLInput
+ return zeroVal, nil
+}
+
+func (ec *executionContext) field_Mutation_enableSAML_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Mutation_enableSAML_argsInput(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["input"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_Mutation_enableSAML_argsInput(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (types.EnableSAMLInput, error) {
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
+ if tmp, ok := rawArgs["input"]; ok {
+ return ec.unmarshalNEnableSAMLInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐEnableSAMLInput(ctx, tmp)
+ }
+
+ var zeroVal types.EnableSAMLInput
+ return zeroVal, nil
+}
+
func (ec *executionContext) field_Mutation_exportDocumentVersionPDF_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -17716,6 +18446,29 @@ func (ec *executionContext) field_Mutation_importMeasure_argsInput(
return zeroVal, nil
}
+func (ec *executionContext) field_Mutation_initiateDomainVerification_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Mutation_initiateDomainVerification_argsInput(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["input"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_Mutation_initiateDomainVerification_argsInput(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (types.InitiateDomainVerificationInput, error) {
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
+ if tmp, ok := rawArgs["input"]; ok {
+ return ec.unmarshalNInitiateDomainVerificationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInitiateDomainVerificationInput(ctx, tmp)
+ }
+
+ var zeroVal types.InitiateDomainVerificationInput
+ return zeroVal, nil
+}
+
func (ec *executionContext) field_Mutation_inviteUser_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -18199,6 +18952,29 @@ func (ec *executionContext) field_Mutation_updateRisk_argsInput(
return zeroVal, nil
}
+func (ec *executionContext) field_Mutation_updateSAMLConfiguration_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Mutation_updateSAMLConfiguration_argsInput(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["input"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_Mutation_updateSAMLConfiguration_argsInput(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (types.UpdateSAMLConfigurationInput, error) {
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
+ if tmp, ok := rawArgs["input"]; ok {
+ return ec.unmarshalNUpdateSAMLConfigurationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateSAMLConfigurationInput(ctx, tmp)
+ }
+
+ var zeroVal types.UpdateSAMLConfigurationInput
+ return zeroVal, nil
+}
+
func (ec *executionContext) field_Mutation_updateTask_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -18567,6 +19343,29 @@ func (ec *executionContext) field_Mutation_uploadVendorDataPrivacyAgreement_args
return zeroVal, nil
}
+func (ec *executionContext) field_Mutation_verifyDomain_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
+ var err error
+ args := map[string]any{}
+ arg0, err := ec.field_Mutation_verifyDomain_argsInput(ctx, rawArgs)
+ if err != nil {
+ return nil, err
+ }
+ args["input"] = arg0
+ return args, nil
+}
+func (ec *executionContext) field_Mutation_verifyDomain_argsInput(
+ ctx context.Context,
+ rawArgs map[string]any,
+) (types.VerifyDomainInput, error) {
+ ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("input"))
+ if tmp, ok := rawArgs["input"]; ok {
+ return ec.unmarshalNVerifyDomainInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVerifyDomainInput(ctx, tmp)
+ }
+
+ var zeroVal types.VerifyDomainInput
+ return zeroVal, nil
+}
+
func (ec *executionContext) field_Organization_assets_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) {
var err error
args := map[string]any{}
@@ -23023,6 +23822,8 @@ func (ec *executionContext) fieldContext_Asset_organization(_ context.Context, f
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -23636,6 +24437,8 @@ func (ec *executionContext) fieldContext_Audit_organization(_ context.Context, f
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -24945,6 +25748,8 @@ func (ec *executionContext) fieldContext_ContinualImprovement_organization(_ con
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -28166,6 +28971,100 @@ func (ec *executionContext) fieldContext_CreateRiskPayload_riskEdge(_ context.Co
return fc, nil
}
+func (ec *executionContext) _CreateSAMLConfigurationPayload_samlConfiguration(ctx context.Context, field graphql.CollectedField, obj *types.CreateSAMLConfigurationPayload) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_CreateSAMLConfigurationPayload_samlConfiguration(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.SamlConfiguration, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.SAMLConfiguration)
+ fc.Result = res
+ return ec.marshalNSAMLConfiguration2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfiguration(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_CreateSAMLConfigurationPayload_samlConfiguration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "CreateSAMLConfigurationPayload",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_SAMLConfiguration_id(ctx, field)
+ case "organization":
+ return ec.fieldContext_SAMLConfiguration_organization(ctx, field)
+ case "emailDomain":
+ return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
+ case "enabled":
+ return ec.fieldContext_SAMLConfiguration_enabled(ctx, field)
+ case "enforcementPolicy":
+ return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
+ case "domainVerified":
+ return ec.fieldContext_SAMLConfiguration_domainVerified(ctx, field)
+ case "domainVerificationToken":
+ return ec.fieldContext_SAMLConfiguration_domainVerificationToken(ctx, field)
+ case "domainVerifiedAt":
+ return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
+ case "spEntityId":
+ return ec.fieldContext_SAMLConfiguration_spEntityId(ctx, field)
+ case "spAcsUrl":
+ return ec.fieldContext_SAMLConfiguration_spAcsUrl(ctx, field)
+ case "spMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_spMetadataUrl(ctx, field)
+ case "idpEntityId":
+ return ec.fieldContext_SAMLConfiguration_idpEntityId(ctx, field)
+ case "idpSsoUrl":
+ return ec.fieldContext_SAMLConfiguration_idpSsoUrl(ctx, field)
+ case "idpCertificate":
+ return ec.fieldContext_SAMLConfiguration_idpCertificate(ctx, field)
+ case "idpMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_idpMetadataUrl(ctx, field)
+ case "attributeEmail":
+ return ec.fieldContext_SAMLConfiguration_attributeEmail(ctx, field)
+ case "attributeFirstname":
+ return ec.fieldContext_SAMLConfiguration_attributeFirstname(ctx, field)
+ case "attributeLastname":
+ return ec.fieldContext_SAMLConfiguration_attributeLastname(ctx, field)
+ case "attributeRole":
+ return ec.fieldContext_SAMLConfiguration_attributeRole(ctx, field)
+ case "defaultRole":
+ return ec.fieldContext_SAMLConfiguration_defaultRole(ctx, field)
+ case "autoSignupEnabled":
+ return ec.fieldContext_SAMLConfiguration_autoSignupEnabled(ctx, field)
+ case "testLoginUrl":
+ return ec.fieldContext_SAMLConfiguration_testLoginUrl(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_SAMLConfiguration_createdAt(ctx, field)
+ case "updatedAt":
+ return ec.fieldContext_SAMLConfiguration_updatedAt(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type SAMLConfiguration", field.Name)
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _CreateSnapshotPayload_snapshotEdge(ctx context.Context, field graphql.CollectedField, obj *types.CreateSnapshotPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_CreateSnapshotPayload_snapshotEdge(ctx, field)
if err != nil {
@@ -28759,6 +29658,8 @@ func (ec *executionContext) fieldContext_CustomDomain_organization(_ context.Con
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -29664,6 +30565,8 @@ func (ec *executionContext) fieldContext_Datum_organization(_ context.Context, f
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -31158,6 +32061,8 @@ func (ec *executionContext) fieldContext_DeleteOrganizationHorizontalLogoPayload
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -31609,6 +32514,50 @@ func (ec *executionContext) fieldContext_DeleteRiskPayload_deletedRiskId(_ conte
return fc, nil
}
+func (ec *executionContext) _DeleteSAMLConfigurationPayload_deletedSAMLConfigurationId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteSAMLConfigurationPayload) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_DeleteSAMLConfigurationPayload_deletedSAMLConfigurationId(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.DeletedSAMLConfigurationID, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(gid.GID)
+ fc.Result = res
+ return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_DeleteSAMLConfigurationPayload_deletedSAMLConfigurationId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "DeleteSAMLConfigurationPayload",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type ID does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _DeleteSnapshotPayload_deletedSnapshotId(ctx context.Context, field graphql.CollectedField, obj *types.DeleteSnapshotPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_DeleteSnapshotPayload_deletedSnapshotId(ctx, field)
if err != nil {
@@ -32157,6 +33106,100 @@ func (ec *executionContext) fieldContext_DeleteVendorServicePayload_deletedVendo
return fc, nil
}
+func (ec *executionContext) _DisableSAMLPayload_samlConfiguration(ctx context.Context, field graphql.CollectedField, obj *types.DisableSAMLPayload) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_DisableSAMLPayload_samlConfiguration(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.SamlConfiguration, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.SAMLConfiguration)
+ fc.Result = res
+ return ec.marshalNSAMLConfiguration2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfiguration(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_DisableSAMLPayload_samlConfiguration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "DisableSAMLPayload",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_SAMLConfiguration_id(ctx, field)
+ case "organization":
+ return ec.fieldContext_SAMLConfiguration_organization(ctx, field)
+ case "emailDomain":
+ return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
+ case "enabled":
+ return ec.fieldContext_SAMLConfiguration_enabled(ctx, field)
+ case "enforcementPolicy":
+ return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
+ case "domainVerified":
+ return ec.fieldContext_SAMLConfiguration_domainVerified(ctx, field)
+ case "domainVerificationToken":
+ return ec.fieldContext_SAMLConfiguration_domainVerificationToken(ctx, field)
+ case "domainVerifiedAt":
+ return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
+ case "spEntityId":
+ return ec.fieldContext_SAMLConfiguration_spEntityId(ctx, field)
+ case "spAcsUrl":
+ return ec.fieldContext_SAMLConfiguration_spAcsUrl(ctx, field)
+ case "spMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_spMetadataUrl(ctx, field)
+ case "idpEntityId":
+ return ec.fieldContext_SAMLConfiguration_idpEntityId(ctx, field)
+ case "idpSsoUrl":
+ return ec.fieldContext_SAMLConfiguration_idpSsoUrl(ctx, field)
+ case "idpCertificate":
+ return ec.fieldContext_SAMLConfiguration_idpCertificate(ctx, field)
+ case "idpMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_idpMetadataUrl(ctx, field)
+ case "attributeEmail":
+ return ec.fieldContext_SAMLConfiguration_attributeEmail(ctx, field)
+ case "attributeFirstname":
+ return ec.fieldContext_SAMLConfiguration_attributeFirstname(ctx, field)
+ case "attributeLastname":
+ return ec.fieldContext_SAMLConfiguration_attributeLastname(ctx, field)
+ case "attributeRole":
+ return ec.fieldContext_SAMLConfiguration_attributeRole(ctx, field)
+ case "defaultRole":
+ return ec.fieldContext_SAMLConfiguration_defaultRole(ctx, field)
+ case "autoSignupEnabled":
+ return ec.fieldContext_SAMLConfiguration_autoSignupEnabled(ctx, field)
+ case "testLoginUrl":
+ return ec.fieldContext_SAMLConfiguration_testLoginUrl(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_SAMLConfiguration_createdAt(ctx, field)
+ case "updatedAt":
+ return ec.fieldContext_SAMLConfiguration_updatedAt(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type SAMLConfiguration", field.Name)
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _Document_id(ctx context.Context, field graphql.CollectedField, obj *types.Document) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Document_id(ctx, field)
if err != nil {
@@ -32627,6 +33670,8 @@ func (ec *executionContext) fieldContext_Document_organization(_ context.Context
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -34579,6 +35624,100 @@ func (ec *executionContext) fieldContext_DocumentVersionSignatureEdge_node(_ con
return fc, nil
}
+func (ec *executionContext) _EnableSAMLPayload_samlConfiguration(ctx context.Context, field graphql.CollectedField, obj *types.EnableSAMLPayload) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_EnableSAMLPayload_samlConfiguration(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.SamlConfiguration, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.SAMLConfiguration)
+ fc.Result = res
+ return ec.marshalNSAMLConfiguration2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfiguration(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_EnableSAMLPayload_samlConfiguration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "EnableSAMLPayload",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_SAMLConfiguration_id(ctx, field)
+ case "organization":
+ return ec.fieldContext_SAMLConfiguration_organization(ctx, field)
+ case "emailDomain":
+ return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
+ case "enabled":
+ return ec.fieldContext_SAMLConfiguration_enabled(ctx, field)
+ case "enforcementPolicy":
+ return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
+ case "domainVerified":
+ return ec.fieldContext_SAMLConfiguration_domainVerified(ctx, field)
+ case "domainVerificationToken":
+ return ec.fieldContext_SAMLConfiguration_domainVerificationToken(ctx, field)
+ case "domainVerifiedAt":
+ return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
+ case "spEntityId":
+ return ec.fieldContext_SAMLConfiguration_spEntityId(ctx, field)
+ case "spAcsUrl":
+ return ec.fieldContext_SAMLConfiguration_spAcsUrl(ctx, field)
+ case "spMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_spMetadataUrl(ctx, field)
+ case "idpEntityId":
+ return ec.fieldContext_SAMLConfiguration_idpEntityId(ctx, field)
+ case "idpSsoUrl":
+ return ec.fieldContext_SAMLConfiguration_idpSsoUrl(ctx, field)
+ case "idpCertificate":
+ return ec.fieldContext_SAMLConfiguration_idpCertificate(ctx, field)
+ case "idpMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_idpMetadataUrl(ctx, field)
+ case "attributeEmail":
+ return ec.fieldContext_SAMLConfiguration_attributeEmail(ctx, field)
+ case "attributeFirstname":
+ return ec.fieldContext_SAMLConfiguration_attributeFirstname(ctx, field)
+ case "attributeLastname":
+ return ec.fieldContext_SAMLConfiguration_attributeLastname(ctx, field)
+ case "attributeRole":
+ return ec.fieldContext_SAMLConfiguration_attributeRole(ctx, field)
+ case "defaultRole":
+ return ec.fieldContext_SAMLConfiguration_defaultRole(ctx, field)
+ case "autoSignupEnabled":
+ return ec.fieldContext_SAMLConfiguration_autoSignupEnabled(ctx, field)
+ case "testLoginUrl":
+ return ec.fieldContext_SAMLConfiguration_testLoginUrl(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_SAMLConfiguration_createdAt(ctx, field)
+ case "updatedAt":
+ return ec.fieldContext_SAMLConfiguration_updatedAt(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type SAMLConfiguration", field.Name)
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _Evidence_id(ctx context.Context, field graphql.CollectedField, obj *types.Evidence) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Evidence_id(ctx, field)
if err != nil {
@@ -36007,6 +37146,8 @@ func (ec *executionContext) fieldContext_Framework_organization(_ context.Contex
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -36721,6 +37862,144 @@ func (ec *executionContext) fieldContext_ImportMeasurePayload_measureEdges(_ con
return fc, nil
}
+func (ec *executionContext) _InitiateDomainVerificationPayload_samlConfiguration(ctx context.Context, field graphql.CollectedField, obj *types.InitiateDomainVerificationPayload) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_InitiateDomainVerificationPayload_samlConfiguration(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.SamlConfiguration, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.SAMLConfiguration)
+ fc.Result = res
+ return ec.marshalNSAMLConfiguration2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfiguration(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_InitiateDomainVerificationPayload_samlConfiguration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "InitiateDomainVerificationPayload",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_SAMLConfiguration_id(ctx, field)
+ case "organization":
+ return ec.fieldContext_SAMLConfiguration_organization(ctx, field)
+ case "emailDomain":
+ return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
+ case "enabled":
+ return ec.fieldContext_SAMLConfiguration_enabled(ctx, field)
+ case "enforcementPolicy":
+ return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
+ case "domainVerified":
+ return ec.fieldContext_SAMLConfiguration_domainVerified(ctx, field)
+ case "domainVerificationToken":
+ return ec.fieldContext_SAMLConfiguration_domainVerificationToken(ctx, field)
+ case "domainVerifiedAt":
+ return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
+ case "spEntityId":
+ return ec.fieldContext_SAMLConfiguration_spEntityId(ctx, field)
+ case "spAcsUrl":
+ return ec.fieldContext_SAMLConfiguration_spAcsUrl(ctx, field)
+ case "spMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_spMetadataUrl(ctx, field)
+ case "idpEntityId":
+ return ec.fieldContext_SAMLConfiguration_idpEntityId(ctx, field)
+ case "idpSsoUrl":
+ return ec.fieldContext_SAMLConfiguration_idpSsoUrl(ctx, field)
+ case "idpCertificate":
+ return ec.fieldContext_SAMLConfiguration_idpCertificate(ctx, field)
+ case "idpMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_idpMetadataUrl(ctx, field)
+ case "attributeEmail":
+ return ec.fieldContext_SAMLConfiguration_attributeEmail(ctx, field)
+ case "attributeFirstname":
+ return ec.fieldContext_SAMLConfiguration_attributeFirstname(ctx, field)
+ case "attributeLastname":
+ return ec.fieldContext_SAMLConfiguration_attributeLastname(ctx, field)
+ case "attributeRole":
+ return ec.fieldContext_SAMLConfiguration_attributeRole(ctx, field)
+ case "defaultRole":
+ return ec.fieldContext_SAMLConfiguration_defaultRole(ctx, field)
+ case "autoSignupEnabled":
+ return ec.fieldContext_SAMLConfiguration_autoSignupEnabled(ctx, field)
+ case "testLoginUrl":
+ return ec.fieldContext_SAMLConfiguration_testLoginUrl(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_SAMLConfiguration_createdAt(ctx, field)
+ case "updatedAt":
+ return ec.fieldContext_SAMLConfiguration_updatedAt(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type SAMLConfiguration", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _InitiateDomainVerificationPayload_dnsRecord(ctx context.Context, field graphql.CollectedField, obj *types.InitiateDomainVerificationPayload) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_InitiateDomainVerificationPayload_dnsRecord(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.DNSRecord, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_InitiateDomainVerificationPayload_dnsRecord(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "InitiateDomainVerificationPayload",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _Invitation_id(ctx context.Context, field graphql.CollectedField, obj *types.Invitation) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Invitation_id(ctx, field)
if err != nil {
@@ -37169,6 +38448,8 @@ func (ec *executionContext) fieldContext_Invitation_organization(_ context.Conte
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -38570,6 +39851,50 @@ func (ec *executionContext) fieldContext_Membership_emailAddress(_ context.Conte
return fc, nil
}
+func (ec *executionContext) _Membership_authMethod(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Membership_authMethod(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Membership().AuthMethod(rctx, obj)
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(coredata.UserAuthMethod)
+ fc.Result = res
+ return ec.marshalNUserAuthMethod2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐUserAuthMethod(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Membership_authMethod(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Membership",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type UserAuthMethod does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _Membership_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Membership) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Membership_createdAt(ctx, field)
if err != nil {
@@ -38901,6 +40226,8 @@ func (ec *executionContext) fieldContext_MembershipEdge_node(_ context.Context,
return ec.fieldContext_Membership_fullName(ctx, field)
case "emailAddress":
return ec.fieldContext_Membership_emailAddress(ctx, field)
+ case "authMethod":
+ return ec.fieldContext_Membership_authMethod(ctx, field)
case "createdAt":
return ec.fieldContext_Membership_createdAt(ctx, field)
case "updatedAt":
@@ -46262,6 +47589,423 @@ func (ec *executionContext) fieldContext_Mutation_deleteCustomDomain(ctx context
return fc, nil
}
+func (ec *executionContext) _Mutation_initiateDomainVerification(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Mutation_initiateDomainVerification(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Mutation().InitiateDomainVerification(rctx, fc.Args["input"].(types.InitiateDomainVerificationInput))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.InitiateDomainVerificationPayload)
+ fc.Result = res
+ return ec.marshalNInitiateDomainVerificationPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInitiateDomainVerificationPayload(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_initiateDomainVerification(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Mutation",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "samlConfiguration":
+ return ec.fieldContext_InitiateDomainVerificationPayload_samlConfiguration(ctx, field)
+ case "dnsRecord":
+ return ec.fieldContext_InitiateDomainVerificationPayload_dnsRecord(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type InitiateDomainVerificationPayload", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Mutation_initiateDomainVerification_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Mutation_verifyDomain(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Mutation_verifyDomain(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Mutation().VerifyDomain(rctx, fc.Args["input"].(types.VerifyDomainInput))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.VerifyDomainPayload)
+ fc.Result = res
+ return ec.marshalNVerifyDomainPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVerifyDomainPayload(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_verifyDomain(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Mutation",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "samlConfiguration":
+ return ec.fieldContext_VerifyDomainPayload_samlConfiguration(ctx, field)
+ case "verified":
+ return ec.fieldContext_VerifyDomainPayload_verified(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type VerifyDomainPayload", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Mutation_verifyDomain_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Mutation_createSAMLConfiguration(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Mutation_createSAMLConfiguration(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Mutation().CreateSAMLConfiguration(rctx, fc.Args["input"].(types.CreateSAMLConfigurationInput))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.CreateSAMLConfigurationPayload)
+ fc.Result = res
+ return ec.marshalNCreateSAMLConfigurationPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateSAMLConfigurationPayload(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_createSAMLConfiguration(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Mutation",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "samlConfiguration":
+ return ec.fieldContext_CreateSAMLConfigurationPayload_samlConfiguration(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type CreateSAMLConfigurationPayload", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Mutation_createSAMLConfiguration_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Mutation_updateSAMLConfiguration(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Mutation_updateSAMLConfiguration(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Mutation().UpdateSAMLConfiguration(rctx, fc.Args["input"].(types.UpdateSAMLConfigurationInput))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.UpdateSAMLConfigurationPayload)
+ fc.Result = res
+ return ec.marshalNUpdateSAMLConfigurationPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateSAMLConfigurationPayload(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_updateSAMLConfiguration(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Mutation",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "samlConfiguration":
+ return ec.fieldContext_UpdateSAMLConfigurationPayload_samlConfiguration(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type UpdateSAMLConfigurationPayload", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Mutation_updateSAMLConfiguration_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Mutation_deleteSAMLConfiguration(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Mutation_deleteSAMLConfiguration(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Mutation().DeleteSAMLConfiguration(rctx, fc.Args["input"].(types.DeleteSAMLConfigurationInput))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.DeleteSAMLConfigurationPayload)
+ fc.Result = res
+ return ec.marshalNDeleteSAMLConfigurationPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteSAMLConfigurationPayload(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_deleteSAMLConfiguration(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Mutation",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "deletedSAMLConfigurationId":
+ return ec.fieldContext_DeleteSAMLConfigurationPayload_deletedSAMLConfigurationId(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type DeleteSAMLConfigurationPayload", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Mutation_deleteSAMLConfiguration_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Mutation_enableSAML(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Mutation_enableSAML(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Mutation().EnableSaml(rctx, fc.Args["input"].(types.EnableSAMLInput))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.EnableSAMLPayload)
+ fc.Result = res
+ return ec.marshalNEnableSAMLPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐEnableSAMLPayload(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_enableSAML(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Mutation",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "samlConfiguration":
+ return ec.fieldContext_EnableSAMLPayload_samlConfiguration(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type EnableSAMLPayload", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Mutation_enableSAML_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _Mutation_disableSAML(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Mutation_disableSAML(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Mutation().DisableSaml(rctx, fc.Args["input"].(types.DisableSAMLInput))
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.DisableSAMLPayload)
+ fc.Result = res
+ return ec.marshalNDisableSAMLPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDisableSAMLPayload(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Mutation_disableSAML(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Mutation",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "samlConfiguration":
+ return ec.fieldContext_DisableSAMLPayload_samlConfiguration(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type DisableSAMLPayload", field.Name)
+ },
+ }
+ defer func() {
+ if r := recover(); r != nil {
+ err = ec.Recover(ctx, r)
+ ec.Error(ctx, err)
+ }
+ }()
+ ctx = graphql.WithFieldContext(ctx, fc)
+ if fc.Args, err = ec.field_Mutation_disableSAML_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
+ ec.Error(ctx, err)
+ return fc, err
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _Nonconformity_id(ctx context.Context, field graphql.CollectedField, obj *types.Nonconformity) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Nonconformity_id(ctx, field)
if err != nil {
@@ -46446,6 +48190,8 @@ func (ec *executionContext) fieldContext_Nonconformity_organization(_ context.Co
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -47513,6 +49259,8 @@ func (ec *executionContext) fieldContext_Obligation_organization(_ context.Conte
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -49989,6 +51737,100 @@ func (ec *executionContext) fieldContext_Organization_customDomain(_ context.Con
return fc, nil
}
+func (ec *executionContext) _Organization_samlConfigurations(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_Organization_samlConfigurations(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.Organization().SamlConfigurations(rctx, obj)
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.([]*types.SAMLConfiguration)
+ fc.Result = res
+ return ec.marshalNSAMLConfiguration2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfigurationᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_Organization_samlConfigurations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Organization",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_SAMLConfiguration_id(ctx, field)
+ case "organization":
+ return ec.fieldContext_SAMLConfiguration_organization(ctx, field)
+ case "emailDomain":
+ return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
+ case "enabled":
+ return ec.fieldContext_SAMLConfiguration_enabled(ctx, field)
+ case "enforcementPolicy":
+ return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
+ case "domainVerified":
+ return ec.fieldContext_SAMLConfiguration_domainVerified(ctx, field)
+ case "domainVerificationToken":
+ return ec.fieldContext_SAMLConfiguration_domainVerificationToken(ctx, field)
+ case "domainVerifiedAt":
+ return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
+ case "spEntityId":
+ return ec.fieldContext_SAMLConfiguration_spEntityId(ctx, field)
+ case "spAcsUrl":
+ return ec.fieldContext_SAMLConfiguration_spAcsUrl(ctx, field)
+ case "spMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_spMetadataUrl(ctx, field)
+ case "idpEntityId":
+ return ec.fieldContext_SAMLConfiguration_idpEntityId(ctx, field)
+ case "idpSsoUrl":
+ return ec.fieldContext_SAMLConfiguration_idpSsoUrl(ctx, field)
+ case "idpCertificate":
+ return ec.fieldContext_SAMLConfiguration_idpCertificate(ctx, field)
+ case "idpMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_idpMetadataUrl(ctx, field)
+ case "attributeEmail":
+ return ec.fieldContext_SAMLConfiguration_attributeEmail(ctx, field)
+ case "attributeFirstname":
+ return ec.fieldContext_SAMLConfiguration_attributeFirstname(ctx, field)
+ case "attributeLastname":
+ return ec.fieldContext_SAMLConfiguration_attributeLastname(ctx, field)
+ case "attributeRole":
+ return ec.fieldContext_SAMLConfiguration_attributeRole(ctx, field)
+ case "defaultRole":
+ return ec.fieldContext_SAMLConfiguration_defaultRole(ctx, field)
+ case "autoSignupEnabled":
+ return ec.fieldContext_SAMLConfiguration_autoSignupEnabled(ctx, field)
+ case "testLoginUrl":
+ return ec.fieldContext_SAMLConfiguration_testLoginUrl(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_SAMLConfiguration_createdAt(ctx, field)
+ case "updatedAt":
+ return ec.fieldContext_SAMLConfiguration_updatedAt(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type SAMLConfiguration", field.Name)
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _Organization_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Organization_createdAt(ctx, field)
if err != nil {
@@ -50324,6 +52166,8 @@ func (ec *executionContext) fieldContext_OrganizationEdge_node(_ context.Context
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -51419,6 +53263,8 @@ func (ec *executionContext) fieldContext_ProcessingActivity_organization(_ conte
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -54108,6 +55954,8 @@ func (ec *executionContext) fieldContext_Risk_organization(_ context.Context, fi
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -54739,6 +56587,1119 @@ func (ec *executionContext) fieldContext_RiskEdge_node(_ context.Context, field
return fc, nil
}
+func (ec *executionContext) _SAMLConfiguration_id(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_id(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.ID, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(gid.GID)
+ fc.Result = res
+ return ec.marshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_id(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type ID does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_organization(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_organization(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.SAMLConfiguration().Organization(rctx, obj)
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.Organization)
+ fc.Result = res
+ return ec.marshalNOrganization2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐOrganization(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_organization(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_Organization_id(ctx, field)
+ case "name":
+ return ec.fieldContext_Organization_name(ctx, field)
+ case "logoUrl":
+ return ec.fieldContext_Organization_logoUrl(ctx, field)
+ case "horizontalLogoUrl":
+ return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field)
+ case "description":
+ return ec.fieldContext_Organization_description(ctx, field)
+ case "websiteUrl":
+ return ec.fieldContext_Organization_websiteUrl(ctx, field)
+ case "email":
+ return ec.fieldContext_Organization_email(ctx, field)
+ case "headquarterAddress":
+ return ec.fieldContext_Organization_headquarterAddress(ctx, field)
+ case "memberships":
+ return ec.fieldContext_Organization_memberships(ctx, field)
+ case "invitations":
+ return ec.fieldContext_Organization_invitations(ctx, field)
+ case "slackConnections":
+ return ec.fieldContext_Organization_slackConnections(ctx, field)
+ case "frameworks":
+ return ec.fieldContext_Organization_frameworks(ctx, field)
+ case "controls":
+ return ec.fieldContext_Organization_controls(ctx, field)
+ case "vendors":
+ return ec.fieldContext_Organization_vendors(ctx, field)
+ case "peoples":
+ return ec.fieldContext_Organization_peoples(ctx, field)
+ case "documents":
+ return ec.fieldContext_Organization_documents(ctx, field)
+ case "measures":
+ return ec.fieldContext_Organization_measures(ctx, field)
+ case "risks":
+ return ec.fieldContext_Organization_risks(ctx, field)
+ case "tasks":
+ return ec.fieldContext_Organization_tasks(ctx, field)
+ case "assets":
+ return ec.fieldContext_Organization_assets(ctx, field)
+ case "data":
+ return ec.fieldContext_Organization_data(ctx, field)
+ case "audits":
+ return ec.fieldContext_Organization_audits(ctx, field)
+ case "nonconformities":
+ return ec.fieldContext_Organization_nonconformities(ctx, field)
+ case "obligations":
+ return ec.fieldContext_Organization_obligations(ctx, field)
+ case "continualImprovements":
+ return ec.fieldContext_Organization_continualImprovements(ctx, field)
+ case "processingActivities":
+ return ec.fieldContext_Organization_processingActivities(ctx, field)
+ case "snapshots":
+ return ec.fieldContext_Organization_snapshots(ctx, field)
+ case "trustCenter":
+ return ec.fieldContext_Organization_trustCenter(ctx, field)
+ case "customDomain":
+ return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_Organization_createdAt(ctx, field)
+ case "updatedAt":
+ return ec.fieldContext_Organization_updatedAt(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_emailDomain(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.EmailDomain, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_emailDomain(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_enabled(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_enabled(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Enabled, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(bool)
+ fc.Result = res
+ return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_enabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_enforcementPolicy(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.EnforcementPolicy, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(coredata.SAMLEnforcementPolicy)
+ fc.Result = res
+ return ec.marshalNSAMLEnforcementPolicy2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_enforcementPolicy(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type SAMLEnforcementPolicy does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_domainVerified(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_domainVerified(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.DomainVerified, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(bool)
+ fc.Result = res
+ return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_domainVerified(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_domainVerificationToken(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_domainVerificationToken(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.DomainVerificationToken, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.(*string)
+ fc.Result = res
+ return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_domainVerificationToken(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_domainVerifiedAt(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.DomainVerifiedAt, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.(*time.Time)
+ fc.Result = res
+ return ec.marshalODatetime2ᚖtimeᚐTime(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_domainVerifiedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Datetime does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_spEntityId(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_spEntityId(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.SpEntityID, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_spEntityId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_spAcsUrl(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_spAcsUrl(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.SpAcsURL, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_spAcsUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_spMetadataUrl(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_spMetadataUrl(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.SAMLConfiguration().SpMetadataURL(rctx, obj)
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_spMetadataUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_idpEntityId(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_idpEntityId(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IdpEntityID, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_idpEntityId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_idpSsoUrl(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_idpSsoUrl(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IdpSsoURL, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_idpSsoUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_idpCertificate(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_idpCertificate(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IdpCertificate, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_idpCertificate(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_idpMetadataUrl(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_idpMetadataUrl(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.IdpMetadataURL, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ return graphql.Null
+ }
+ res := resTmp.(*string)
+ fc.Result = res
+ return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_idpMetadataUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_attributeEmail(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_attributeEmail(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.AttributeEmail, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_attributeEmail(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_attributeFirstname(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_attributeFirstname(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.AttributeFirstname, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_attributeFirstname(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_attributeLastname(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_attributeLastname(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.AttributeLastname, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_attributeLastname(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_attributeRole(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_attributeRole(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.AttributeRole, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_attributeRole(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_defaultRole(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_defaultRole(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.DefaultRole, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_defaultRole(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_autoSignupEnabled(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_autoSignupEnabled(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.AutoSignupEnabled, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(bool)
+ fc.Result = res
+ return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_autoSignupEnabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_testLoginUrl(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_testLoginUrl(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return ec.resolvers.SAMLConfiguration().TestLoginURL(rctx, obj)
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(string)
+ fc.Result = res
+ return ec.marshalNString2string(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_testLoginUrl(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type String does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_createdAt(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_createdAt(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.CreatedAt, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(time.Time)
+ fc.Result = res
+ return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_createdAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Datetime does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _SAMLConfiguration_updatedAt(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_SAMLConfiguration_updatedAt(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.UpdatedAt, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(time.Time)
+ fc.Result = res
+ return ec.marshalNDatetime2timeᚐTime(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_SAMLConfiguration_updatedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "SAMLConfiguration",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Datetime does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _SendSigningNotificationsPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.SendSigningNotificationsPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_SendSigningNotificationsPayload_success(ctx, field)
if err != nil {
@@ -55432,6 +58393,8 @@ func (ec *executionContext) fieldContext_Snapshot_organization(_ context.Context
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -56351,6 +59314,8 @@ func (ec *executionContext) fieldContext_Task_organization(_ context.Context, fi
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -57197,6 +60162,8 @@ func (ec *executionContext) fieldContext_TrustCenter_organization(_ context.Cont
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -61004,6 +63971,8 @@ func (ec *executionContext) fieldContext_UpdateOrganizationPayload_organization(
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -61257,6 +64226,100 @@ func (ec *executionContext) fieldContext_UpdateRiskPayload_risk(_ context.Contex
return fc, nil
}
+func (ec *executionContext) _UpdateSAMLConfigurationPayload_samlConfiguration(ctx context.Context, field graphql.CollectedField, obj *types.UpdateSAMLConfigurationPayload) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_UpdateSAMLConfigurationPayload_samlConfiguration(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.SamlConfiguration, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.SAMLConfiguration)
+ fc.Result = res
+ return ec.marshalNSAMLConfiguration2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfiguration(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_UpdateSAMLConfigurationPayload_samlConfiguration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "UpdateSAMLConfigurationPayload",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_SAMLConfiguration_id(ctx, field)
+ case "organization":
+ return ec.fieldContext_SAMLConfiguration_organization(ctx, field)
+ case "emailDomain":
+ return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
+ case "enabled":
+ return ec.fieldContext_SAMLConfiguration_enabled(ctx, field)
+ case "enforcementPolicy":
+ return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
+ case "domainVerified":
+ return ec.fieldContext_SAMLConfiguration_domainVerified(ctx, field)
+ case "domainVerificationToken":
+ return ec.fieldContext_SAMLConfiguration_domainVerificationToken(ctx, field)
+ case "domainVerifiedAt":
+ return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
+ case "spEntityId":
+ return ec.fieldContext_SAMLConfiguration_spEntityId(ctx, field)
+ case "spAcsUrl":
+ return ec.fieldContext_SAMLConfiguration_spAcsUrl(ctx, field)
+ case "spMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_spMetadataUrl(ctx, field)
+ case "idpEntityId":
+ return ec.fieldContext_SAMLConfiguration_idpEntityId(ctx, field)
+ case "idpSsoUrl":
+ return ec.fieldContext_SAMLConfiguration_idpSsoUrl(ctx, field)
+ case "idpCertificate":
+ return ec.fieldContext_SAMLConfiguration_idpCertificate(ctx, field)
+ case "idpMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_idpMetadataUrl(ctx, field)
+ case "attributeEmail":
+ return ec.fieldContext_SAMLConfiguration_attributeEmail(ctx, field)
+ case "attributeFirstname":
+ return ec.fieldContext_SAMLConfiguration_attributeFirstname(ctx, field)
+ case "attributeLastname":
+ return ec.fieldContext_SAMLConfiguration_attributeLastname(ctx, field)
+ case "attributeRole":
+ return ec.fieldContext_SAMLConfiguration_attributeRole(ctx, field)
+ case "defaultRole":
+ return ec.fieldContext_SAMLConfiguration_defaultRole(ctx, field)
+ case "autoSignupEnabled":
+ return ec.fieldContext_SAMLConfiguration_autoSignupEnabled(ctx, field)
+ case "testLoginUrl":
+ return ec.fieldContext_SAMLConfiguration_testLoginUrl(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_SAMLConfiguration_createdAt(ctx, field)
+ case "updatedAt":
+ return ec.fieldContext_SAMLConfiguration_updatedAt(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type SAMLConfiguration", field.Name)
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _UpdateTaskPayload_task(ctx context.Context, field graphql.CollectedField, obj *types.UpdateTaskPayload) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_UpdateTaskPayload_task(ctx, field)
if err != nil {
@@ -63084,6 +66147,8 @@ func (ec *executionContext) fieldContext_Vendor_organization(_ context.Context,
return ec.fieldContext_Organization_trustCenter(ctx, field)
case "customDomain":
return ec.fieldContext_Organization_customDomain(ctx, field)
+ case "samlConfigurations":
+ return ec.fieldContext_Organization_samlConfigurations(ctx, field)
case "createdAt":
return ec.fieldContext_Organization_createdAt(ctx, field)
case "updatedAt":
@@ -67911,6 +70976,144 @@ func (ec *executionContext) fieldContext_VendorServiceEdge_node(_ context.Contex
return fc, nil
}
+func (ec *executionContext) _VerifyDomainPayload_samlConfiguration(ctx context.Context, field graphql.CollectedField, obj *types.VerifyDomainPayload) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_VerifyDomainPayload_samlConfiguration(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.SamlConfiguration, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(*types.SAMLConfiguration)
+ fc.Result = res
+ return ec.marshalNSAMLConfiguration2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfiguration(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_VerifyDomainPayload_samlConfiguration(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "VerifyDomainPayload",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_SAMLConfiguration_id(ctx, field)
+ case "organization":
+ return ec.fieldContext_SAMLConfiguration_organization(ctx, field)
+ case "emailDomain":
+ return ec.fieldContext_SAMLConfiguration_emailDomain(ctx, field)
+ case "enabled":
+ return ec.fieldContext_SAMLConfiguration_enabled(ctx, field)
+ case "enforcementPolicy":
+ return ec.fieldContext_SAMLConfiguration_enforcementPolicy(ctx, field)
+ case "domainVerified":
+ return ec.fieldContext_SAMLConfiguration_domainVerified(ctx, field)
+ case "domainVerificationToken":
+ return ec.fieldContext_SAMLConfiguration_domainVerificationToken(ctx, field)
+ case "domainVerifiedAt":
+ return ec.fieldContext_SAMLConfiguration_domainVerifiedAt(ctx, field)
+ case "spEntityId":
+ return ec.fieldContext_SAMLConfiguration_spEntityId(ctx, field)
+ case "spAcsUrl":
+ return ec.fieldContext_SAMLConfiguration_spAcsUrl(ctx, field)
+ case "spMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_spMetadataUrl(ctx, field)
+ case "idpEntityId":
+ return ec.fieldContext_SAMLConfiguration_idpEntityId(ctx, field)
+ case "idpSsoUrl":
+ return ec.fieldContext_SAMLConfiguration_idpSsoUrl(ctx, field)
+ case "idpCertificate":
+ return ec.fieldContext_SAMLConfiguration_idpCertificate(ctx, field)
+ case "idpMetadataUrl":
+ return ec.fieldContext_SAMLConfiguration_idpMetadataUrl(ctx, field)
+ case "attributeEmail":
+ return ec.fieldContext_SAMLConfiguration_attributeEmail(ctx, field)
+ case "attributeFirstname":
+ return ec.fieldContext_SAMLConfiguration_attributeFirstname(ctx, field)
+ case "attributeLastname":
+ return ec.fieldContext_SAMLConfiguration_attributeLastname(ctx, field)
+ case "attributeRole":
+ return ec.fieldContext_SAMLConfiguration_attributeRole(ctx, field)
+ case "defaultRole":
+ return ec.fieldContext_SAMLConfiguration_defaultRole(ctx, field)
+ case "autoSignupEnabled":
+ return ec.fieldContext_SAMLConfiguration_autoSignupEnabled(ctx, field)
+ case "testLoginUrl":
+ return ec.fieldContext_SAMLConfiguration_testLoginUrl(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_SAMLConfiguration_createdAt(ctx, field)
+ case "updatedAt":
+ return ec.fieldContext_SAMLConfiguration_updatedAt(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type SAMLConfiguration", field.Name)
+ },
+ }
+ return fc, nil
+}
+
+func (ec *executionContext) _VerifyDomainPayload_verified(ctx context.Context, field graphql.CollectedField, obj *types.VerifyDomainPayload) (ret graphql.Marshaler) {
+ fc, err := ec.fieldContext_VerifyDomainPayload_verified(ctx, field)
+ if err != nil {
+ return graphql.Null
+ }
+ ctx = graphql.WithFieldContext(ctx, fc)
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = graphql.Null
+ }
+ }()
+ resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) {
+ ctx = rctx // use context from middleware stack in children
+ return obj.Verified, nil
+ })
+ if err != nil {
+ ec.Error(ctx, err)
+ return graphql.Null
+ }
+ if resTmp == nil {
+ if !graphql.HasFieldError(ctx, fc) {
+ ec.Errorf(ctx, "must not be null")
+ }
+ return graphql.Null
+ }
+ res := resTmp.(bool)
+ fc.Result = res
+ return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) fieldContext_VerifyDomainPayload_verified(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "VerifyDomainPayload",
+ Field: field,
+ IsMethod: false,
+ IsResolver: false,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ return nil, errors.New("field of type Boolean does not have child fields")
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _Viewer_id(ctx context.Context, field graphql.CollectedField, obj *types.Viewer) (ret graphql.Marshaler) {
fc, err := ec.fieldContext_Viewer_id(ctx, field)
if err != nil {
@@ -71964,6 +75167,138 @@ func (ec *executionContext) unmarshalInputCreateRiskObligationMappingInput(ctx c
return it, nil
}
+func (ec *executionContext) unmarshalInputCreateSAMLConfigurationInput(ctx context.Context, obj any) (types.CreateSAMLConfigurationInput, error) {
+ var it types.CreateSAMLConfigurationInput
+ asMap := map[string]any{}
+ for k, v := range obj.(map[string]any) {
+ asMap[k] = v
+ }
+
+ fieldsInOrder := [...]string{"organizationId", "emailDomain", "enforcementPolicy", "spCertificate", "spPrivateKey", "idpMetadataXml", "idpEntityId", "idpSsoUrl", "idpCertificate", "idpMetadataUrl", "attributeEmail", "attributeFirstname", "attributeLastname", "attributeRole", "defaultRole", "autoSignupEnabled"}
+ for _, k := range fieldsInOrder {
+ v, ok := asMap[k]
+ if !ok {
+ continue
+ }
+ switch k {
+ case "organizationId":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId"))
+ data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.OrganizationID = data
+ case "emailDomain":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("emailDomain"))
+ data, err := ec.unmarshalNString2string(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.EmailDomain = data
+ case "enforcementPolicy":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("enforcementPolicy"))
+ data, err := ec.unmarshalNSAMLEnforcementPolicy2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.EnforcementPolicy = data
+ case "spCertificate":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("spCertificate"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.SpCertificate = data
+ case "spPrivateKey":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("spPrivateKey"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.SpPrivateKey = data
+ case "idpMetadataXml":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idpMetadataXml"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.IdpMetadataXML = data
+ case "idpEntityId":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idpEntityId"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.IdpEntityID = data
+ case "idpSsoUrl":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idpSsoUrl"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.IdpSsoURL = data
+ case "idpCertificate":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idpCertificate"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.IdpCertificate = data
+ case "idpMetadataUrl":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idpMetadataUrl"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.IdpMetadataURL = data
+ case "attributeEmail":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("attributeEmail"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.AttributeEmail = data
+ case "attributeFirstname":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("attributeFirstname"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.AttributeFirstname = data
+ case "attributeLastname":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("attributeLastname"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.AttributeLastname = data
+ case "attributeRole":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("attributeRole"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.AttributeRole = data
+ case "defaultRole":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("defaultRole"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.DefaultRole = data
+ case "autoSignupEnabled":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("autoSignupEnabled"))
+ data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.AutoSignupEnabled = data
+ }
+ }
+
+ return it, nil
+}
+
func (ec *executionContext) unmarshalInputCreateSnapshotInput(ctx context.Context, obj any) (types.CreateSnapshotInput, error) {
var it types.CreateSnapshotInput
asMap := map[string]any{}
@@ -73403,6 +76738,33 @@ func (ec *executionContext) unmarshalInputDeleteRiskObligationMappingInput(ctx c
return it, nil
}
+func (ec *executionContext) unmarshalInputDeleteSAMLConfigurationInput(ctx context.Context, obj any) (types.DeleteSAMLConfigurationInput, error) {
+ var it types.DeleteSAMLConfigurationInput
+ asMap := map[string]any{}
+ for k, v := range obj.(map[string]any) {
+ asMap[k] = v
+ }
+
+ fieldsInOrder := [...]string{"id"}
+ for _, k := range fieldsInOrder {
+ v, ok := asMap[k]
+ if !ok {
+ continue
+ }
+ switch k {
+ case "id":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
+ data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.ID = data
+ }
+ }
+
+ return it, nil
+}
+
func (ec *executionContext) unmarshalInputDeleteSnapshotInput(ctx context.Context, obj any) (types.DeleteSnapshotInput, error) {
var it types.DeleteSnapshotInput
asMap := map[string]any{}
@@ -73727,6 +77089,33 @@ func (ec *executionContext) unmarshalInputDeleteVendorServiceInput(ctx context.C
return it, nil
}
+func (ec *executionContext) unmarshalInputDisableSAMLInput(ctx context.Context, obj any) (types.DisableSAMLInput, error) {
+ var it types.DisableSAMLInput
+ asMap := map[string]any{}
+ for k, v := range obj.(map[string]any) {
+ asMap[k] = v
+ }
+
+ fieldsInOrder := [...]string{"id"}
+ for _, k := range fieldsInOrder {
+ v, ok := asMap[k]
+ if !ok {
+ continue
+ }
+ switch k {
+ case "id":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
+ data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.ID = data
+ }
+ }
+
+ return it, nil
+}
+
func (ec *executionContext) unmarshalInputDocumentFilter(ctx context.Context, obj any) (types.DocumentFilter, error) {
var it types.DocumentFilter
asMap := map[string]any{}
@@ -73910,6 +77299,33 @@ func (ec *executionContext) unmarshalInputDocumentVersionSignatureOrder(ctx cont
return it, nil
}
+func (ec *executionContext) unmarshalInputEnableSAMLInput(ctx context.Context, obj any) (types.EnableSAMLInput, error) {
+ var it types.EnableSAMLInput
+ asMap := map[string]any{}
+ for k, v := range obj.(map[string]any) {
+ asMap[k] = v
+ }
+
+ fieldsInOrder := [...]string{"id"}
+ for _, k := range fieldsInOrder {
+ v, ok := asMap[k]
+ if !ok {
+ continue
+ }
+ switch k {
+ case "id":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
+ data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.ID = data
+ }
+ }
+
+ return it, nil
+}
+
func (ec *executionContext) unmarshalInputEvidenceOrder(ctx context.Context, obj any) (types.EvidenceOrderBy, error) {
var it types.EvidenceOrderBy
asMap := map[string]any{}
@@ -74250,6 +77666,40 @@ func (ec *executionContext) unmarshalInputImportMeasureInput(ctx context.Context
return it, nil
}
+func (ec *executionContext) unmarshalInputInitiateDomainVerificationInput(ctx context.Context, obj any) (types.InitiateDomainVerificationInput, error) {
+ var it types.InitiateDomainVerificationInput
+ asMap := map[string]any{}
+ for k, v := range obj.(map[string]any) {
+ asMap[k] = v
+ }
+
+ fieldsInOrder := [...]string{"organizationId", "emailDomain"}
+ for _, k := range fieldsInOrder {
+ v, ok := asMap[k]
+ if !ok {
+ continue
+ }
+ switch k {
+ case "organizationId":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationId"))
+ data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.OrganizationID = data
+ case "emailDomain":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("emailDomain"))
+ data, err := ec.unmarshalNString2string(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.EmailDomain = data
+ }
+ }
+
+ return it, nil
+}
+
func (ec *executionContext) unmarshalInputInvitationFilter(ctx context.Context, obj any) (types.InvitationFilter, error) {
var it types.InvitationFilter
asMap := map[string]any{}
@@ -76306,6 +79756,131 @@ func (ec *executionContext) unmarshalInputUpdateRiskInput(ctx context.Context, o
return it, nil
}
+func (ec *executionContext) unmarshalInputUpdateSAMLConfigurationInput(ctx context.Context, obj any) (types.UpdateSAMLConfigurationInput, error) {
+ var it types.UpdateSAMLConfigurationInput
+ asMap := map[string]any{}
+ for k, v := range obj.(map[string]any) {
+ asMap[k] = v
+ }
+
+ fieldsInOrder := [...]string{"id", "enabled", "enforcementPolicy", "spCertificate", "spPrivateKey", "idpEntityId", "idpSsoUrl", "idpCertificate", "idpMetadataUrl", "attributeEmail", "attributeFirstname", "attributeLastname", "attributeRole", "defaultRole", "autoSignupEnabled"}
+ for _, k := range fieldsInOrder {
+ v, ok := asMap[k]
+ if !ok {
+ continue
+ }
+ switch k {
+ case "id":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
+ data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.ID = data
+ case "enabled":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("enabled"))
+ data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.Enabled = data
+ case "enforcementPolicy":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("enforcementPolicy"))
+ data, err := ec.unmarshalOSAMLEnforcementPolicy2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.EnforcementPolicy = data
+ case "spCertificate":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("spCertificate"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.SpCertificate = data
+ case "spPrivateKey":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("spPrivateKey"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.SpPrivateKey = data
+ case "idpEntityId":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idpEntityId"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.IdpEntityID = data
+ case "idpSsoUrl":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idpSsoUrl"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.IdpSsoURL = data
+ case "idpCertificate":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idpCertificate"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.IdpCertificate = data
+ case "idpMetadataUrl":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("idpMetadataUrl"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.IdpMetadataURL = data
+ case "attributeEmail":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("attributeEmail"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.AttributeEmail = data
+ case "attributeFirstname":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("attributeFirstname"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.AttributeFirstname = data
+ case "attributeLastname":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("attributeLastname"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.AttributeLastname = data
+ case "attributeRole":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("attributeRole"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.AttributeRole = data
+ case "defaultRole":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("defaultRole"))
+ data, err := ec.unmarshalOString2ᚖstring(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.DefaultRole = data
+ case "autoSignupEnabled":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("autoSignupEnabled"))
+ data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.AutoSignupEnabled = data
+ }
+ }
+
+ return it, nil
+}
+
func (ec *executionContext) unmarshalInputUpdateTaskInput(ctx context.Context, obj any) (types.UpdateTaskInput, error) {
var it types.UpdateTaskInput
asMap := map[string]any{}
@@ -77438,6 +81013,33 @@ func (ec *executionContext) unmarshalInputVendorServiceOrder(ctx context.Context
return it, nil
}
+func (ec *executionContext) unmarshalInputVerifyDomainInput(ctx context.Context, obj any) (types.VerifyDomainInput, error) {
+ var it types.VerifyDomainInput
+ asMap := map[string]any{}
+ for k, v := range obj.(map[string]any) {
+ asMap[k] = v
+ }
+
+ fieldsInOrder := [...]string{"id"}
+ for _, k := range fieldsInOrder {
+ v, ok := asMap[k]
+ if !ok {
+ continue
+ }
+ switch k {
+ case "id":
+ ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
+ data, err := ec.unmarshalNID2githubᚗcomᚋgetproboᚋproboᚋpkgᚋgidᚐGID(ctx, v)
+ if err != nil {
+ return it, err
+ }
+ it.ID = data
+ }
+ }
+
+ return it, nil
+}
+
// endregion **************************** input.gotpl *****************************
// region ************************** interface.gotpl ***************************
@@ -77544,6 +81146,13 @@ func (ec *executionContext) _Node(ctx context.Context, sel ast.SelectionSet, obj
return graphql.Null
}
return ec._Snapshot(ctx, sel, obj)
+ case types.SAMLConfiguration:
+ return ec._SAMLConfiguration(ctx, sel, &obj)
+ case *types.SAMLConfiguration:
+ if obj == nil {
+ return graphql.Null
+ }
+ return ec._SAMLConfiguration(ctx, sel, obj)
case types.Risk:
return ec._Risk(ctx, sel, &obj)
case *types.Risk:
@@ -80374,6 +83983,45 @@ func (ec *executionContext) _CreateRiskPayload(ctx context.Context, sel ast.Sele
return out
}
+var createSAMLConfigurationPayloadImplementors = []string{"CreateSAMLConfigurationPayload"}
+
+func (ec *executionContext) _CreateSAMLConfigurationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreateSAMLConfigurationPayload) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, createSAMLConfigurationPayloadImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("CreateSAMLConfigurationPayload")
+ case "samlConfiguration":
+ out.Values[i] = ec._CreateSAMLConfigurationPayload_samlConfiguration(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
var createSnapshotPayloadImplementors = []string{"CreateSnapshotPayload"}
func (ec *executionContext) _CreateSnapshotPayload(ctx context.Context, sel ast.SelectionSet, obj *types.CreateSnapshotPayload) graphql.Marshaler {
@@ -82236,6 +85884,45 @@ func (ec *executionContext) _DeleteRiskPayload(ctx context.Context, sel ast.Sele
return out
}
+var deleteSAMLConfigurationPayloadImplementors = []string{"DeleteSAMLConfigurationPayload"}
+
+func (ec *executionContext) _DeleteSAMLConfigurationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteSAMLConfigurationPayload) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, deleteSAMLConfigurationPayloadImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("DeleteSAMLConfigurationPayload")
+ case "deletedSAMLConfigurationId":
+ out.Values[i] = ec._DeleteSAMLConfigurationPayload_deletedSAMLConfigurationId(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
var deleteSnapshotPayloadImplementors = []string{"DeleteSnapshotPayload"}
func (ec *executionContext) _DeleteSnapshotPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DeleteSnapshotPayload) graphql.Marshaler {
@@ -82704,6 +86391,45 @@ func (ec *executionContext) _DeleteVendorServicePayload(ctx context.Context, sel
return out
}
+var disableSAMLPayloadImplementors = []string{"DisableSAMLPayload"}
+
+func (ec *executionContext) _DisableSAMLPayload(ctx context.Context, sel ast.SelectionSet, obj *types.DisableSAMLPayload) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, disableSAMLPayloadImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("DisableSAMLPayload")
+ case "samlConfiguration":
+ out.Values[i] = ec._DisableSAMLPayload_samlConfiguration(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
var documentImplementors = []string{"Document", "Node"}
func (ec *executionContext) _Document(ctx context.Context, sel ast.SelectionSet, obj *types.Document) graphql.Marshaler {
@@ -83546,6 +87272,45 @@ func (ec *executionContext) _DocumentVersionSignatureEdge(ctx context.Context, s
return out
}
+var enableSAMLPayloadImplementors = []string{"EnableSAMLPayload"}
+
+func (ec *executionContext) _EnableSAMLPayload(ctx context.Context, sel ast.SelectionSet, obj *types.EnableSAMLPayload) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, enableSAMLPayloadImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("EnableSAMLPayload")
+ case "samlConfiguration":
+ out.Values[i] = ec._EnableSAMLPayload_samlConfiguration(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
var evidenceImplementors = []string{"Evidence", "Node"}
func (ec *executionContext) _Evidence(ctx context.Context, sel ast.SelectionSet, obj *types.Evidence) graphql.Marshaler {
@@ -84510,6 +88275,50 @@ func (ec *executionContext) _ImportMeasurePayload(ctx context.Context, sel ast.S
return out
}
+var initiateDomainVerificationPayloadImplementors = []string{"InitiateDomainVerificationPayload"}
+
+func (ec *executionContext) _InitiateDomainVerificationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.InitiateDomainVerificationPayload) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, initiateDomainVerificationPayloadImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("InitiateDomainVerificationPayload")
+ case "samlConfiguration":
+ out.Values[i] = ec._InitiateDomainVerificationPayload_samlConfiguration(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "dnsRecord":
+ out.Values[i] = ec._InitiateDomainVerificationPayload_dnsRecord(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
var invitationImplementors = []string{"Invitation", "Node"}
func (ec *executionContext) _Invitation(ctx context.Context, sel ast.SelectionSet, obj *types.Invitation) graphql.Marshaler {
@@ -85131,42 +88940,78 @@ func (ec *executionContext) _Membership(ctx context.Context, sel ast.SelectionSe
case "id":
out.Values[i] = ec._Membership_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "userID":
out.Values[i] = ec._Membership_userID(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "organizationID":
out.Values[i] = ec._Membership_organizationID(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "role":
out.Values[i] = ec._Membership_role(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "fullName":
out.Values[i] = ec._Membership_fullName(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "emailAddress":
out.Values[i] = ec._Membership_emailAddress(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
+ case "authMethod":
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._Membership_authMethod(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "createdAt":
out.Values[i] = ec._Membership_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
case "updatedAt":
out.Values[i] = ec._Membership_updatedAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
- out.Invalids++
+ atomic.AddUint32(&out.Invalids, 1)
}
default:
panic("unknown field " + strconv.Quote(field.Name))
@@ -86202,6 +90047,55 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
out.Invalids++
}
+ case "initiateDomainVerification":
+ out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+ return ec._Mutation_initiateDomainVerification(ctx, field)
+ })
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "verifyDomain":
+ out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+ return ec._Mutation_verifyDomain(ctx, field)
+ })
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "createSAMLConfiguration":
+ out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+ return ec._Mutation_createSAMLConfiguration(ctx, field)
+ })
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "updateSAMLConfiguration":
+ out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+ return ec._Mutation_updateSAMLConfiguration(ctx, field)
+ })
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "deleteSAMLConfiguration":
+ out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+ return ec._Mutation_deleteSAMLConfiguration(ctx, field)
+ })
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "enableSAML":
+ out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+ return ec._Mutation_enableSAML(ctx, field)
+ })
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "disableSAML":
+ out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) {
+ return ec._Mutation_disableSAML(ctx, field)
+ })
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -87681,6 +91575,42 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection
continue
}
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ case "samlConfigurations":
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._Organization_samlConfigurations(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
case "createdAt":
out.Values[i] = ec._Organization_createdAt(ctx, field, obj)
@@ -89144,6 +93074,244 @@ func (ec *executionContext) _RiskEdge(ctx context.Context, sel ast.SelectionSet,
return out
}
+var sAMLConfigurationImplementors = []string{"SAMLConfiguration", "Node"}
+
+func (ec *executionContext) _SAMLConfiguration(ctx context.Context, sel ast.SelectionSet, obj *types.SAMLConfiguration) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, sAMLConfigurationImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("SAMLConfiguration")
+ case "id":
+ out.Values[i] = ec._SAMLConfiguration_id(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "organization":
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._SAMLConfiguration_organization(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ case "emailDomain":
+ out.Values[i] = ec._SAMLConfiguration_emailDomain(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "enabled":
+ out.Values[i] = ec._SAMLConfiguration_enabled(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "enforcementPolicy":
+ out.Values[i] = ec._SAMLConfiguration_enforcementPolicy(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "domainVerified":
+ out.Values[i] = ec._SAMLConfiguration_domainVerified(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "domainVerificationToken":
+ out.Values[i] = ec._SAMLConfiguration_domainVerificationToken(ctx, field, obj)
+ case "domainVerifiedAt":
+ out.Values[i] = ec._SAMLConfiguration_domainVerifiedAt(ctx, field, obj)
+ case "spEntityId":
+ out.Values[i] = ec._SAMLConfiguration_spEntityId(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "spAcsUrl":
+ out.Values[i] = ec._SAMLConfiguration_spAcsUrl(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "spMetadataUrl":
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._SAMLConfiguration_spMetadataUrl(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ case "idpEntityId":
+ out.Values[i] = ec._SAMLConfiguration_idpEntityId(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "idpSsoUrl":
+ out.Values[i] = ec._SAMLConfiguration_idpSsoUrl(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "idpCertificate":
+ out.Values[i] = ec._SAMLConfiguration_idpCertificate(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "idpMetadataUrl":
+ out.Values[i] = ec._SAMLConfiguration_idpMetadataUrl(ctx, field, obj)
+ case "attributeEmail":
+ out.Values[i] = ec._SAMLConfiguration_attributeEmail(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "attributeFirstname":
+ out.Values[i] = ec._SAMLConfiguration_attributeFirstname(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "attributeLastname":
+ out.Values[i] = ec._SAMLConfiguration_attributeLastname(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "attributeRole":
+ out.Values[i] = ec._SAMLConfiguration_attributeRole(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "defaultRole":
+ out.Values[i] = ec._SAMLConfiguration_defaultRole(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "autoSignupEnabled":
+ out.Values[i] = ec._SAMLConfiguration_autoSignupEnabled(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "testLoginUrl":
+ field := field
+
+ innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._SAMLConfiguration_testLoginUrl(ctx, field, obj)
+ if res == graphql.Null {
+ atomic.AddUint32(&fs.Invalids, 1)
+ }
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ case "createdAt":
+ out.Values[i] = ec._SAMLConfiguration_createdAt(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ case "updatedAt":
+ out.Values[i] = ec._SAMLConfiguration_updatedAt(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ atomic.AddUint32(&out.Invalids, 1)
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
var sendSigningNotificationsPayloadImplementors = []string{"SendSigningNotificationsPayload"}
func (ec *executionContext) _SendSigningNotificationsPayload(ctx context.Context, sel ast.SelectionSet, obj *types.SendSigningNotificationsPayload) graphql.Marshaler {
@@ -91882,6 +96050,45 @@ func (ec *executionContext) _UpdateRiskPayload(ctx context.Context, sel ast.Sele
return out
}
+var updateSAMLConfigurationPayloadImplementors = []string{"UpdateSAMLConfigurationPayload"}
+
+func (ec *executionContext) _UpdateSAMLConfigurationPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateSAMLConfigurationPayload) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, updateSAMLConfigurationPayloadImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("UpdateSAMLConfigurationPayload")
+ case "samlConfiguration":
+ out.Values[i] = ec._UpdateSAMLConfigurationPayload_samlConfiguration(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
var updateTaskPayloadImplementors = []string{"UpdateTaskPayload"}
func (ec *executionContext) _UpdateTaskPayload(ctx context.Context, sel ast.SelectionSet, obj *types.UpdateTaskPayload) graphql.Marshaler {
@@ -94266,6 +98473,50 @@ func (ec *executionContext) _VendorServiceEdge(ctx context.Context, sel ast.Sele
return out
}
+var verifyDomainPayloadImplementors = []string{"VerifyDomainPayload"}
+
+func (ec *executionContext) _VerifyDomainPayload(ctx context.Context, sel ast.SelectionSet, obj *types.VerifyDomainPayload) graphql.Marshaler {
+ fields := graphql.CollectFields(ec.OperationContext, sel, verifyDomainPayloadImplementors)
+
+ out := graphql.NewFieldSet(fields)
+ deferred := make(map[string]*graphql.FieldSet)
+ for i, field := range fields {
+ switch field.Name {
+ case "__typename":
+ out.Values[i] = graphql.MarshalString("VerifyDomainPayload")
+ case "samlConfiguration":
+ out.Values[i] = ec._VerifyDomainPayload_samlConfiguration(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ case "verified":
+ out.Values[i] = ec._VerifyDomainPayload_verified(ctx, field, obj)
+ if out.Values[i] == graphql.Null {
+ out.Invalids++
+ }
+ default:
+ panic("unknown field " + strconv.Quote(field.Name))
+ }
+ }
+ out.Dispatch(ctx)
+ if out.Invalids > 0 {
+ return graphql.Null
+ }
+
+ atomic.AddInt32(&ec.deferred, int32(len(deferred)))
+
+ for label, dfs := range deferred {
+ ec.processDeferredGroup(graphql.DeferredGroup{
+ Label: label,
+ Path: graphql.GetPath(ctx),
+ FieldSet: dfs,
+ Context: ctx,
+ })
+ }
+
+ return out
+}
+
var viewerImplementors = []string{"Viewer"}
func (ec *executionContext) _Viewer(ctx context.Context, sel ast.SelectionSet, obj *types.Viewer) graphql.Marshaler {
@@ -97063,6 +101314,25 @@ func (ec *executionContext) marshalNCreateRiskPayload2ᚖgithubᚗcomᚋgetprobo
return ec._CreateRiskPayload(ctx, sel, v)
}
+func (ec *executionContext) unmarshalNCreateSAMLConfigurationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateSAMLConfigurationInput(ctx context.Context, v any) (types.CreateSAMLConfigurationInput, error) {
+ res, err := ec.unmarshalInputCreateSAMLConfigurationInput(ctx, v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNCreateSAMLConfigurationPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateSAMLConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v types.CreateSAMLConfigurationPayload) graphql.Marshaler {
+ return ec._CreateSAMLConfigurationPayload(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNCreateSAMLConfigurationPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateSAMLConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v *types.CreateSAMLConfigurationPayload) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._CreateSAMLConfigurationPayload(ctx, sel, v)
+}
+
func (ec *executionContext) unmarshalNCreateSnapshotInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐCreateSnapshotInput(ctx context.Context, v any) (types.CreateSnapshotInput, error) {
res, err := ec.unmarshalInputCreateSnapshotInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -98017,6 +102287,25 @@ func (ec *executionContext) marshalNDeleteRiskPayload2ᚖgithubᚗcomᚋgetprobo
return ec._DeleteRiskPayload(ctx, sel, v)
}
+func (ec *executionContext) unmarshalNDeleteSAMLConfigurationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteSAMLConfigurationInput(ctx context.Context, v any) (types.DeleteSAMLConfigurationInput, error) {
+ res, err := ec.unmarshalInputDeleteSAMLConfigurationInput(ctx, v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNDeleteSAMLConfigurationPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteSAMLConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v types.DeleteSAMLConfigurationPayload) graphql.Marshaler {
+ return ec._DeleteSAMLConfigurationPayload(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNDeleteSAMLConfigurationPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteSAMLConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v *types.DeleteSAMLConfigurationPayload) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._DeleteSAMLConfigurationPayload(ctx, sel, v)
+}
+
func (ec *executionContext) unmarshalNDeleteSnapshotInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDeleteSnapshotInput(ctx context.Context, v any) (types.DeleteSnapshotInput, error) {
res, err := ec.unmarshalInputDeleteSnapshotInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -98245,6 +102534,25 @@ func (ec *executionContext) marshalNDeleteVendorServicePayload2ᚖgithubᚗcom
return ec._DeleteVendorServicePayload(ctx, sel, v)
}
+func (ec *executionContext) unmarshalNDisableSAMLInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDisableSAMLInput(ctx context.Context, v any) (types.DisableSAMLInput, error) {
+ res, err := ec.unmarshalInputDisableSAMLInput(ctx, v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNDisableSAMLPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDisableSAMLPayload(ctx context.Context, sel ast.SelectionSet, v types.DisableSAMLPayload) graphql.Marshaler {
+ return ec._DisableSAMLPayload(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNDisableSAMLPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDisableSAMLPayload(ctx context.Context, sel ast.SelectionSet, v *types.DisableSAMLPayload) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._DisableSAMLPayload(ctx, sel, v)
+}
+
func (ec *executionContext) marshalNDocument2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐDocument(ctx context.Context, sel ast.SelectionSet, v types.Document) graphql.Marshaler {
return ec._Document(ctx, sel, &v)
}
@@ -98693,6 +103001,25 @@ var (
}
)
+func (ec *executionContext) unmarshalNEnableSAMLInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐEnableSAMLInput(ctx context.Context, v any) (types.EnableSAMLInput, error) {
+ res, err := ec.unmarshalInputEnableSAMLInput(ctx, v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNEnableSAMLPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐEnableSAMLPayload(ctx context.Context, sel ast.SelectionSet, v types.EnableSAMLPayload) graphql.Marshaler {
+ return ec._EnableSAMLPayload(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNEnableSAMLPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐEnableSAMLPayload(ctx context.Context, sel ast.SelectionSet, v *types.EnableSAMLPayload) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._EnableSAMLPayload(ctx, sel, v)
+}
+
func (ec *executionContext) marshalNEvidence2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐEvidence(ctx context.Context, sel ast.SelectionSet, v *types.Evidence) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
@@ -99131,6 +103458,25 @@ func (ec *executionContext) marshalNImportMeasurePayload2ᚖgithubᚗcomᚋgetpr
return ec._ImportMeasurePayload(ctx, sel, v)
}
+func (ec *executionContext) unmarshalNInitiateDomainVerificationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInitiateDomainVerificationInput(ctx context.Context, v any) (types.InitiateDomainVerificationInput, error) {
+ res, err := ec.unmarshalInputInitiateDomainVerificationInput(ctx, v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNInitiateDomainVerificationPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInitiateDomainVerificationPayload(ctx context.Context, sel ast.SelectionSet, v types.InitiateDomainVerificationPayload) graphql.Marshaler {
+ return ec._InitiateDomainVerificationPayload(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNInitiateDomainVerificationPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐInitiateDomainVerificationPayload(ctx context.Context, sel ast.SelectionSet, v *types.InitiateDomainVerificationPayload) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._InitiateDomainVerificationPayload(ctx, sel, v)
+}
+
func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v any) (int, error) {
res, err := graphql.UnmarshalInt(v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -100585,6 +104931,90 @@ var (
}
)
+func (ec *executionContext) marshalNSAMLConfiguration2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfigurationᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.SAMLConfiguration) graphql.Marshaler {
+ ret := make(graphql.Array, len(v))
+ var wg sync.WaitGroup
+ isLen1 := len(v) == 1
+ if !isLen1 {
+ wg.Add(len(v))
+ }
+ for i := range v {
+ i := i
+ fc := &graphql.FieldContext{
+ Index: &i,
+ Result: &v[i],
+ }
+ ctx := graphql.WithFieldContext(ctx, fc)
+ f := func(i int) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ ret = nil
+ }
+ }()
+ if !isLen1 {
+ defer wg.Done()
+ }
+ ret[i] = ec.marshalNSAMLConfiguration2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfiguration(ctx, sel, v[i])
+ }
+ if isLen1 {
+ f(i)
+ } else {
+ go f(i)
+ }
+
+ }
+ wg.Wait()
+
+ for _, e := range ret {
+ if e == graphql.Null {
+ return graphql.Null
+ }
+ }
+
+ return ret
+}
+
+func (ec *executionContext) marshalNSAMLConfiguration2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSAMLConfiguration(ctx context.Context, sel ast.SelectionSet, v *types.SAMLConfiguration) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._SAMLConfiguration(ctx, sel, v)
+}
+
+func (ec *executionContext) unmarshalNSAMLEnforcementPolicy2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy(ctx context.Context, v any) (coredata.SAMLEnforcementPolicy, error) {
+ tmp, err := graphql.UnmarshalString(v)
+ res := unmarshalNSAMLEnforcementPolicy2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy[tmp]
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNSAMLEnforcementPolicy2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy(ctx context.Context, sel ast.SelectionSet, v coredata.SAMLEnforcementPolicy) graphql.Marshaler {
+ _ = sel
+ res := graphql.MarshalString(marshalNSAMLEnforcementPolicy2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy[v])
+ if res == graphql.Null {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ }
+ return res
+}
+
+var (
+ unmarshalNSAMLEnforcementPolicy2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy = map[string]coredata.SAMLEnforcementPolicy{
+ "OFF": coredata.SAMLEnforcementPolicyOff,
+ "OPTIONAL": coredata.SAMLEnforcementPolicyOptional,
+ "REQUIRED": coredata.SAMLEnforcementPolicyRequired,
+ }
+ marshalNSAMLEnforcementPolicy2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy = map[coredata.SAMLEnforcementPolicy]string{
+ coredata.SAMLEnforcementPolicyOff: "OFF",
+ coredata.SAMLEnforcementPolicyOptional: "OPTIONAL",
+ coredata.SAMLEnforcementPolicyRequired: "REQUIRED",
+ }
+)
+
func (ec *executionContext) unmarshalNSSLStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐCustomDomainSSLStatus(ctx context.Context, v any) (coredata.CustomDomainSSLStatus, error) {
tmp, err := graphql.UnmarshalString(v)
res := unmarshalNSSLStatus2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐCustomDomainSSLStatus[tmp]
@@ -101859,6 +106289,25 @@ func (ec *executionContext) marshalNUpdateRiskPayload2ᚖgithubᚗcomᚋgetprobo
return ec._UpdateRiskPayload(ctx, sel, v)
}
+func (ec *executionContext) unmarshalNUpdateSAMLConfigurationInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateSAMLConfigurationInput(ctx context.Context, v any) (types.UpdateSAMLConfigurationInput, error) {
+ res, err := ec.unmarshalInputUpdateSAMLConfigurationInput(ctx, v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNUpdateSAMLConfigurationPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateSAMLConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v types.UpdateSAMLConfigurationPayload) graphql.Marshaler {
+ return ec._UpdateSAMLConfigurationPayload(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNUpdateSAMLConfigurationPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateSAMLConfigurationPayload(ctx context.Context, sel ast.SelectionSet, v *types.UpdateSAMLConfigurationPayload) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._UpdateSAMLConfigurationPayload(ctx, sel, v)
+}
+
func (ec *executionContext) unmarshalNUpdateTaskInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUpdateTaskInput(ctx context.Context, v any) (types.UpdateTaskInput, error) {
res, err := ec.unmarshalInputUpdateTaskInput(ctx, v)
return res, graphql.ErrorOnPath(ctx, err)
@@ -102189,6 +106638,34 @@ func (ec *executionContext) marshalNUser2ᚖgithubᚗcomᚋgetproboᚋproboᚋpk
return ec._User(ctx, sel, v)
}
+func (ec *executionContext) unmarshalNUserAuthMethod2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐUserAuthMethod(ctx context.Context, v any) (coredata.UserAuthMethod, error) {
+ tmp, err := graphql.UnmarshalString(v)
+ res := unmarshalNUserAuthMethod2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐUserAuthMethod[tmp]
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNUserAuthMethod2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐUserAuthMethod(ctx context.Context, sel ast.SelectionSet, v coredata.UserAuthMethod) graphql.Marshaler {
+ _ = sel
+ res := graphql.MarshalString(marshalNUserAuthMethod2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐUserAuthMethod[v])
+ if res == graphql.Null {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ }
+ return res
+}
+
+var (
+ unmarshalNUserAuthMethod2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐUserAuthMethod = map[string]coredata.UserAuthMethod{
+ "PASSWORD": coredata.UserAuthMethodPassword,
+ "SAML": coredata.UserAuthMethodSAML,
+ }
+ marshalNUserAuthMethod2githubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐUserAuthMethod = map[coredata.UserAuthMethod]string{
+ coredata.UserAuthMethodPassword: "PASSWORD",
+ coredata.UserAuthMethodSAML: "SAML",
+ }
+)
+
func (ec *executionContext) marshalNUserEdge2ᚕᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐUserEdgeᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.UserEdge) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
@@ -102895,6 +107372,25 @@ var (
}
)
+func (ec *executionContext) unmarshalNVerifyDomainInput2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVerifyDomainInput(ctx context.Context, v any) (types.VerifyDomainInput, error) {
+ res, err := ec.unmarshalInputVerifyDomainInput(ctx, v)
+ return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNVerifyDomainPayload2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVerifyDomainPayload(ctx context.Context, sel ast.SelectionSet, v types.VerifyDomainPayload) graphql.Marshaler {
+ return ec._VerifyDomainPayload(ctx, sel, &v)
+}
+
+func (ec *executionContext) marshalNVerifyDomainPayload2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐVerifyDomainPayload(ctx context.Context, sel ast.SelectionSet, v *types.VerifyDomainPayload) graphql.Marshaler {
+ if v == nil {
+ if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+ ec.Errorf(ctx, "the requested element is null which the schema does not allow")
+ }
+ return graphql.Null
+ }
+ return ec._VerifyDomainPayload(ctx, sel, v)
+}
+
func (ec *executionContext) marshalNViewer2githubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐViewer(ctx context.Context, sel ast.SelectionSet, v types.Viewer) graphql.Marshaler {
return ec._Viewer(ctx, sel, &v)
}
@@ -104979,6 +109475,38 @@ var (
}
)
+func (ec *executionContext) unmarshalOSAMLEnforcementPolicy2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy(ctx context.Context, v any) (*coredata.SAMLEnforcementPolicy, error) {
+ if v == nil {
+ return nil, nil
+ }
+ tmp, err := graphql.UnmarshalString(v)
+ res := unmarshalOSAMLEnforcementPolicy2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy[tmp]
+ return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalOSAMLEnforcementPolicy2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy(ctx context.Context, sel ast.SelectionSet, v *coredata.SAMLEnforcementPolicy) graphql.Marshaler {
+ if v == nil {
+ return graphql.Null
+ }
+ _ = sel
+ _ = ctx
+ res := graphql.MarshalString(marshalOSAMLEnforcementPolicy2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy[*v])
+ return res
+}
+
+var (
+ unmarshalOSAMLEnforcementPolicy2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy = map[string]coredata.SAMLEnforcementPolicy{
+ "OFF": coredata.SAMLEnforcementPolicyOff,
+ "OPTIONAL": coredata.SAMLEnforcementPolicyOptional,
+ "REQUIRED": coredata.SAMLEnforcementPolicyRequired,
+ }
+ marshalOSAMLEnforcementPolicy2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy = map[coredata.SAMLEnforcementPolicy]string{
+ coredata.SAMLEnforcementPolicyOff: "OFF",
+ coredata.SAMLEnforcementPolicyOptional: "OPTIONAL",
+ coredata.SAMLEnforcementPolicyRequired: "REQUIRED",
+ }
+)
+
func (ec *executionContext) unmarshalOSnapshotOrder2ᚖgithubᚗcomᚋgetproboᚋproboᚋpkgᚋserverᚋapiᚋconsoleᚋv1ᚋtypesᚐSnapshotOrderBy(ctx context.Context, v any) (*types.SnapshotOrderBy, error) {
if v == nil {
return nil, nil
diff --git a/pkg/server/api/console/v1/types/saml_configuration.go b/pkg/server/api/console/v1/types/saml_configuration.go
new file mode 100644
index 000000000..297cb290d
--- /dev/null
+++ b/pkg/server/api/console/v1/types/saml_configuration.go
@@ -0,0 +1,45 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 types
+
+import (
+ "github.com/getprobo/probo/pkg/coredata"
+)
+
+func NewSAMLConfigurationWithURLs(c *coredata.SAMLConfiguration, spEntityID, spAcsURL string) *SAMLConfiguration {
+ return &SAMLConfiguration{
+ ID: c.ID,
+ EmailDomain: c.EmailDomain,
+ Enabled: c.Enabled,
+ EnforcementPolicy: c.EnforcementPolicy,
+ DomainVerified: c.DomainVerified,
+ DomainVerificationToken: c.DomainVerificationToken,
+ DomainVerifiedAt: c.DomainVerifiedAt,
+ SpEntityID: spEntityID,
+ SpAcsURL: spAcsURL,
+ IdpEntityID: c.IdPEntityID,
+ IdpSsoURL: c.IdPSsoURL,
+ IdpCertificate: c.IdPCertificate,
+ IdpMetadataURL: c.IdPMetadataURL,
+ AttributeEmail: c.AttributeEmail,
+ AttributeFirstname: c.AttributeFirstname,
+ AttributeLastname: c.AttributeLastname,
+ AttributeRole: c.AttributeRole,
+ DefaultRole: c.DefaultRole,
+ AutoSignupEnabled: c.AutoSignupEnabled,
+ CreatedAt: c.CreatedAt,
+ UpdatedAt: c.UpdatedAt,
+ }
+}
diff --git a/pkg/server/api/console/v1/types/types.go b/pkg/server/api/console/v1/types/types.go
index 204921333..81e238b8d 100644
--- a/pkg/server/api/console/v1/types/types.go
+++ b/pkg/server/api/console/v1/types/types.go
@@ -505,6 +505,29 @@ type CreateRiskPayload struct {
RiskEdge *RiskEdge `json:"riskEdge"`
}
+type CreateSAMLConfigurationInput struct {
+ OrganizationID gid.GID `json:"organizationId"`
+ EmailDomain string `json:"emailDomain"`
+ EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
+ SpCertificate *string `json:"spCertificate,omitempty"`
+ SpPrivateKey *string `json:"spPrivateKey,omitempty"`
+ IdpMetadataXML *string `json:"idpMetadataXml,omitempty"`
+ IdpEntityID *string `json:"idpEntityId,omitempty"`
+ IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
+ IdpCertificate *string `json:"idpCertificate,omitempty"`
+ IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
+ AttributeEmail *string `json:"attributeEmail,omitempty"`
+ AttributeFirstname *string `json:"attributeFirstname,omitempty"`
+ AttributeLastname *string `json:"attributeLastname,omitempty"`
+ AttributeRole *string `json:"attributeRole,omitempty"`
+ DefaultRole *string `json:"defaultRole,omitempty"`
+ AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
+}
+
+type CreateSAMLConfigurationPayload struct {
+ SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
+}
+
type CreateSnapshotInput struct {
OrganizationID gid.GID `json:"organizationId"`
Name string `json:"name"`
@@ -904,6 +927,14 @@ type DeleteRiskPayload struct {
DeletedRiskID gid.GID `json:"deletedRiskId"`
}
+type DeleteSAMLConfigurationInput struct {
+ ID gid.GID `json:"id"`
+}
+
+type DeleteSAMLConfigurationPayload struct {
+ DeletedSAMLConfigurationID gid.GID `json:"deletedSAMLConfigurationId"`
+}
+
type DeleteSnapshotInput struct {
SnapshotID gid.GID `json:"snapshotId"`
}
@@ -1000,6 +1031,14 @@ type DeleteVendorServicePayload struct {
DeletedVendorServiceID gid.GID `json:"deletedVendorServiceId"`
}
+type DisableSAMLInput struct {
+ ID gid.GID `json:"id"`
+}
+
+type DisableSAMLPayload struct {
+ SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
+}
+
type Document struct {
ID gid.GID `json:"id"`
Title string `json:"title"`
@@ -1094,6 +1133,14 @@ type DocumentVersionSignatureOrder struct {
Direction page.OrderDirection `json:"direction"`
}
+type EnableSAMLInput struct {
+ ID gid.GID `json:"id"`
+}
+
+type EnableSAMLPayload struct {
+ SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
+}
+
type Evidence struct {
ID gid.GID `json:"id"`
Size int `json:"size"`
@@ -1216,6 +1263,16 @@ type ImportMeasurePayload struct {
MeasureEdges []*MeasureEdge `json:"measureEdges"`
}
+type InitiateDomainVerificationInput struct {
+ OrganizationID gid.GID `json:"organizationId"`
+ EmailDomain string `json:"emailDomain"`
+}
+
+type InitiateDomainVerificationPayload struct {
+ SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
+ DNSRecord string `json:"dnsRecord"`
+}
+
type Invitation struct {
ID gid.GID `json:"id"`
Email string `json:"email"`
@@ -1284,14 +1341,15 @@ type MeasureFilter struct {
}
type Membership struct {
- ID gid.GID `json:"id"`
- UserID gid.GID `json:"userID"`
- OrganizationID gid.GID `json:"organizationID"`
- Role string `json:"role"`
- FullName string `json:"fullName"`
- EmailAddress string `json:"emailAddress"`
- CreatedAt time.Time `json:"createdAt"`
- UpdatedAt time.Time `json:"updatedAt"`
+ ID gid.GID `json:"id"`
+ UserID gid.GID `json:"userID"`
+ OrganizationID gid.GID `json:"organizationID"`
+ Role string `json:"role"`
+ FullName string `json:"fullName"`
+ EmailAddress string `json:"emailAddress"`
+ AuthMethod coredata.UserAuthMethod `json:"authMethod"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
}
func (Membership) IsNode() {}
@@ -1396,6 +1454,7 @@ type Organization struct {
TrustCenterFiles *TrustCenterFileConnection `json:"trustCenterFiles"`
TrustCenter *TrustCenter `json:"trustCenter,omitempty"`
CustomDomain *CustomDomain `json:"customDomain,omitempty"`
+ SamlConfigurations []*SAMLConfiguration `json:"samlConfigurations"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
@@ -1580,6 +1639,36 @@ type RiskFilter struct {
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
}
+type SAMLConfiguration struct {
+ ID gid.GID `json:"id"`
+ Organization *Organization `json:"organization"`
+ EmailDomain string `json:"emailDomain"`
+ Enabled bool `json:"enabled"`
+ EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
+ DomainVerified bool `json:"domainVerified"`
+ DomainVerificationToken *string `json:"domainVerificationToken,omitempty"`
+ DomainVerifiedAt *time.Time `json:"domainVerifiedAt,omitempty"`
+ SpEntityID string `json:"spEntityId"`
+ SpAcsURL string `json:"spAcsUrl"`
+ SpMetadataURL string `json:"spMetadataUrl"`
+ IdpEntityID string `json:"idpEntityId"`
+ IdpSsoURL string `json:"idpSsoUrl"`
+ IdpCertificate string `json:"idpCertificate"`
+ IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
+ AttributeEmail string `json:"attributeEmail"`
+ AttributeFirstname string `json:"attributeFirstname"`
+ AttributeLastname string `json:"attributeLastname"`
+ AttributeRole string `json:"attributeRole"`
+ DefaultRole string `json:"defaultRole"`
+ AutoSignupEnabled bool `json:"autoSignupEnabled"`
+ TestLoginURL string `json:"testLoginUrl"`
+ CreatedAt time.Time `json:"createdAt"`
+ UpdatedAt time.Time `json:"updatedAt"`
+}
+
+func (SAMLConfiguration) IsNode() {}
+func (this SAMLConfiguration) GetID() gid.GID { return this.ID }
+
type SendSigningNotificationsInput struct {
OrganizationID gid.GID `json:"organizationId"`
}
@@ -1973,6 +2062,28 @@ type UpdateRiskPayload struct {
Risk *Risk `json:"risk"`
}
+type UpdateSAMLConfigurationInput struct {
+ ID gid.GID `json:"id"`
+ Enabled *bool `json:"enabled,omitempty"`
+ EnforcementPolicy *coredata.SAMLEnforcementPolicy `json:"enforcementPolicy,omitempty"`
+ SpCertificate *string `json:"spCertificate,omitempty"`
+ SpPrivateKey *string `json:"spPrivateKey,omitempty"`
+ IdpEntityID *string `json:"idpEntityId,omitempty"`
+ IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
+ IdpCertificate *string `json:"idpCertificate,omitempty"`
+ IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
+ AttributeEmail *string `json:"attributeEmail,omitempty"`
+ AttributeFirstname *string `json:"attributeFirstname,omitempty"`
+ AttributeLastname *string `json:"attributeLastname,omitempty"`
+ AttributeRole *string `json:"attributeRole,omitempty"`
+ DefaultRole *string `json:"defaultRole,omitempty"`
+ AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
+}
+
+type UpdateSAMLConfigurationPayload struct {
+ SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
+}
+
type UpdateTaskInput struct {
TaskID gid.GID `json:"taskId"`
Name *string `json:"name,omitempty"`
@@ -2359,6 +2470,15 @@ type VendorServiceEdge struct {
Node *VendorService `json:"node"`
}
+type VerifyDomainInput struct {
+ ID gid.GID `json:"id"`
+}
+
+type VerifyDomainPayload struct {
+ SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
+ Verified bool `json:"verified"`
+}
+
type Viewer struct {
ID gid.GID `json:"id"`
User *User `json:"user"`
diff --git a/pkg/server/api/console/v1/v1_resolver.go b/pkg/server/api/console/v1/v1_resolver.go
index 664e3210a..663503e3b 100644
--- a/pkg/server/api/console/v1/v1_resolver.go
+++ b/pkg/server/api/console/v1/v1_resolver.go
@@ -10,8 +10,10 @@ import (
"encoding/json"
"errors"
"fmt"
+ "strings"
"time"
+ "github.com/getprobo/probo/pkg/auth"
"github.com/getprobo/probo/pkg/authz"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/gid"
@@ -1081,6 +1083,20 @@ func (r *measureConnectionResolver) TotalCount(ctx context.Context, obj *types.M
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
+// AuthMethod is the resolver for the authMethod field.
+func (r *membershipResolver) AuthMethod(ctx context.Context, obj *types.Membership) (coredata.UserAuthMethod, error) {
+ session := SessionFromContext(ctx)
+ if session == nil {
+ return coredata.UserAuthMethodPassword, nil
+ }
+
+ authMethod, err := r.authSvc.GetUserAuthMethod(ctx, coredata.NewScope(obj.UserID.TenantID()), obj.UserID, obj.OrganizationID, session)
+ if err != nil {
+ return "", fmt.Errorf("cannot get user auth method: %w", err)
+ }
+ return authMethod, nil
+}
+
// TotalCount is the resolver for the totalCount field.
func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *types.MembershipConnection) (int, error) {
switch obj.Resolver.(type) {
@@ -1098,6 +1114,8 @@ func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *type
// CreateOrganization is the resolver for the createOrganization field.
func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) {
+ currentUser := UserFromContext(ctx)
+
prb := r.proboSvc.WithTenant(gid.NewTenantID())
organization, err := prb.Organizations.Create(
@@ -1112,7 +1130,7 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C
err = r.authzSvc.AddUserToOrganization(
ctx,
- UserFromContext(ctx).ID,
+ currentUser.ID,
organization.ID,
string(authz.RoleMember),
)
@@ -1129,8 +1147,8 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C
ctx,
probo.CreatePeopleRequest{
OrganizationID: organization.ID,
- FullName: UserFromContext(ctx).FullName,
- PrimaryEmailAddress: UserFromContext(ctx).EmailAddress,
+ FullName: currentUser.FullName,
+ PrimaryEmailAddress: currentUser.EmailAddress,
AdditionalEmailAddresses: []string{},
Kind: coredata.PeopleKindEmployee,
},
@@ -3537,6 +3555,260 @@ func (r *mutationResolver) DeleteCustomDomain(ctx context.Context, input types.D
}, nil
}
+// InitiateDomainVerification is the resolver for the initiateDomainVerification field.
+func (r *mutationResolver) InitiateDomainVerification(ctx context.Context, input types.InitiateDomainVerificationInput) (*types.InitiateDomainVerificationPayload, error) {
+ user := UserFromContext(ctx)
+ if user == nil {
+ return nil, fmt.Errorf("user not authenticated")
+ }
+
+ organizationID := input.OrganizationID
+ tenantID := organizationID.TenantID()
+
+ config, err := r.authSvc.InitiateDomainVerification(ctx, tenantID, organizationID, input.EmailDomain)
+ if err != nil {
+ return nil, fmt.Errorf("failed to initiate domain verification: %w", err)
+ }
+
+ dnsRecord := auth.GetDomainVerificationRecord(*config.DomainVerificationToken)
+
+ return &types.InitiateDomainVerificationPayload{
+ SamlConfiguration: types.NewSAMLConfigurationWithURLs(
+ config,
+ r.samlSvc.GetEntityID(),
+ r.samlSvc.GetAcsURL(),
+ ),
+ DNSRecord: dnsRecord,
+ }, nil
+}
+
+// VerifyDomain is the resolver for the verifyDomain field.
+func (r *mutationResolver) VerifyDomain(ctx context.Context, input types.VerifyDomainInput) (*types.VerifyDomainPayload, error) {
+ user := UserFromContext(ctx)
+ if user == nil {
+ return nil, fmt.Errorf("user not authenticated")
+ }
+
+ configID := input.ID
+ tenantID := configID.TenantID()
+
+ config, verified, err := r.authSvc.VerifyDomain(ctx, tenantID, configID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to verify domain: %w", err)
+ }
+
+ return &types.VerifyDomainPayload{
+ SamlConfiguration: types.NewSAMLConfigurationWithURLs(
+ config,
+ r.samlSvc.GetEntityID(),
+ r.samlSvc.GetAcsURL(),
+ ),
+ Verified: verified,
+ }, nil
+}
+
+// CreateSAMLConfiguration is the resolver for the createSAMLConfiguration field.
+func (r *mutationResolver) CreateSAMLConfiguration(ctx context.Context, input types.CreateSAMLConfigurationInput) (*types.CreateSAMLConfigurationPayload, error) {
+ user := UserFromContext(ctx)
+ if user == nil {
+ return nil, fmt.Errorf("user not authenticated")
+ }
+
+ organizationID := input.OrganizationID
+ tenantID := organizationID.TenantID()
+
+ var idpEntityID, idpSsoURL, idpCertificate string
+ var idpMetadataURL *string
+
+ if input.IdpMetadataXML != nil && *input.IdpMetadataXML != "" {
+ metadata, err := auth.ParseIdPMetadata(*input.IdpMetadataXML)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse IdP metadata XML: %w", err)
+ }
+ idpEntityID = metadata.EntityID
+ idpSsoURL = metadata.SsoURL
+ idpCertificate = metadata.Certificate
+ idpMetadataURL = metadata.MetadataURL
+ } else {
+ if input.IdpEntityID == nil || *input.IdpEntityID == "" {
+ return nil, fmt.Errorf("either idpMetadataXml or idpEntityId must be provided")
+ }
+ if input.IdpSsoURL == nil || *input.IdpSsoURL == "" {
+ return nil, fmt.Errorf("either idpMetadataXml or idpSsoUrl must be provided")
+ }
+ if input.IdpCertificate == nil || *input.IdpCertificate == "" {
+ return nil, fmt.Errorf("either idpMetadataXml or idpCertificate must be provided")
+ }
+ idpEntityID = *input.IdpEntityID
+ idpSsoURL = *input.IdpSsoURL
+ idpCertificate = *input.IdpCertificate
+ idpMetadataURL = input.IdpMetadataURL
+ }
+
+ attributeEmail := "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
+ if input.AttributeEmail != nil {
+ attributeEmail = *input.AttributeEmail
+ }
+
+ attributeFirstname := "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
+ if input.AttributeFirstname != nil {
+ attributeFirstname = *input.AttributeFirstname
+ }
+
+ attributeLastname := "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
+ if input.AttributeLastname != nil {
+ attributeLastname = *input.AttributeLastname
+ }
+
+ attributeRole := "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role"
+ if input.AttributeRole != nil {
+ attributeRole = *input.AttributeRole
+ }
+
+ defaultRole := "MEMBER"
+ if input.DefaultRole != nil {
+ defaultRole = *input.DefaultRole
+ }
+
+ autoSignupEnabled := false
+ if input.AutoSignupEnabled != nil {
+ autoSignupEnabled = *input.AutoSignupEnabled
+ }
+
+ config, err := r.authSvc.WithTenant(tenantID).CreateSAMLConfiguration(ctx, auth.CreateSAMLConfigurationRequest{
+ OrganizationID: organizationID,
+ EmailDomain: input.EmailDomain,
+ EnforcementPolicy: input.EnforcementPolicy,
+ IdPEntityID: idpEntityID,
+ IdPSsoURL: idpSsoURL,
+ IdPCertificate: idpCertificate,
+ IdPMetadataURL: idpMetadataURL,
+ AttributeEmail: attributeEmail,
+ AttributeFirstname: attributeFirstname,
+ AttributeLastname: attributeLastname,
+ AttributeRole: attributeRole,
+ DefaultRole: defaultRole,
+ AutoSignupEnabled: autoSignupEnabled,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create SAML configuration: %w", err)
+ }
+
+ return &types.CreateSAMLConfigurationPayload{
+ SamlConfiguration: types.NewSAMLConfigurationWithURLs(
+ config,
+ r.samlSvc.GetEntityID(),
+ r.samlSvc.GetAcsURL(),
+ ),
+ }, nil
+}
+
+// UpdateSAMLConfiguration is the resolver for the updateSAMLConfiguration field.
+func (r *mutationResolver) UpdateSAMLConfiguration(ctx context.Context, input types.UpdateSAMLConfigurationInput) (*types.UpdateSAMLConfigurationPayload, error) {
+ user := UserFromContext(ctx)
+ if user == nil {
+ return nil, fmt.Errorf("user not authenticated")
+ }
+
+ configID := input.ID
+ tenantID := configID.TenantID()
+
+ updatedConfig, err := r.authSvc.WithTenant(tenantID).UpdateSAMLConfiguration(ctx, auth.UpdateSAMLConfigurationRequest{
+ ID: configID,
+ Enabled: input.Enabled,
+ EnforcementPolicy: input.EnforcementPolicy,
+ IdPEntityID: input.IdpEntityID,
+ IdPSsoURL: input.IdpSsoURL,
+ IdPCertificate: input.IdpCertificate,
+ IdPMetadataURL: input.IdpMetadataURL,
+ AttributeEmail: input.AttributeEmail,
+ AttributeFirstname: input.AttributeFirstname,
+ AttributeLastname: input.AttributeLastname,
+ AttributeRole: input.AttributeRole,
+ DefaultRole: input.DefaultRole,
+ AutoSignupEnabled: input.AutoSignupEnabled,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to update SAML configuration: %w", err)
+ }
+
+ return &types.UpdateSAMLConfigurationPayload{
+ SamlConfiguration: types.NewSAMLConfigurationWithURLs(
+ updatedConfig,
+ r.samlSvc.GetEntityID(),
+ r.samlSvc.GetAcsURL(),
+ ),
+ }, nil
+}
+
+// DeleteSAMLConfiguration is the resolver for the deleteSAMLConfiguration field.
+func (r *mutationResolver) DeleteSAMLConfiguration(ctx context.Context, input types.DeleteSAMLConfigurationInput) (*types.DeleteSAMLConfigurationPayload, error) {
+ user := UserFromContext(ctx)
+ if user == nil {
+ return nil, fmt.Errorf("user not authenticated")
+ }
+
+ configID := input.ID
+ tenantID := configID.TenantID()
+
+ err := r.authSvc.WithTenant(tenantID).DeleteSAMLConfiguration(ctx, configID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to delete SAML configuration: %w", err)
+ }
+
+ return &types.DeleteSAMLConfigurationPayload{
+ DeletedSAMLConfigurationID: configID,
+ }, nil
+}
+
+// EnableSaml is the resolver for the enableSAML field.
+func (r *mutationResolver) EnableSaml(ctx context.Context, input types.EnableSAMLInput) (*types.EnableSAMLPayload, error) {
+ user := UserFromContext(ctx)
+ if user == nil {
+ return nil, fmt.Errorf("user not authenticated")
+ }
+
+ configID := input.ID
+ tenantID := configID.TenantID()
+
+ enabledConfig, err := r.authSvc.WithTenant(tenantID).EnableSAMLConfiguration(ctx, configID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to enable SAML: %w", err)
+ }
+
+ return &types.EnableSAMLPayload{
+ SamlConfiguration: types.NewSAMLConfigurationWithURLs(
+ enabledConfig,
+ r.samlSvc.GetEntityID(),
+ r.samlSvc.GetAcsURL(),
+ ),
+ }, nil
+}
+
+// DisableSaml is the resolver for the disableSAML field.
+func (r *mutationResolver) DisableSaml(ctx context.Context, input types.DisableSAMLInput) (*types.DisableSAMLPayload, error) {
+ user := UserFromContext(ctx)
+ if user == nil {
+ return nil, fmt.Errorf("user not authenticated")
+ }
+
+ configID := input.ID
+ tenantID := configID.TenantID()
+
+ disabledConfig, err := r.authSvc.WithTenant(tenantID).DisableSAMLConfiguration(ctx, configID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to disable SAML: %w", err)
+ }
+
+ return &types.DisableSAMLPayload{
+ SamlConfiguration: types.NewSAMLConfigurationWithURLs(
+ disabledConfig,
+ r.samlSvc.GetEntityID(),
+ r.samlSvc.GetAcsURL(),
+ ),
+ }, nil
+}
+
// Organization is the resolver for the organization field.
func (r *nonconformityResolver) Organization(ctx context.Context, obj *types.Nonconformity) (*types.Organization, error) {
prb := r.ProboService(ctx, obj.ID.TenantID())
@@ -4282,6 +4554,27 @@ func (r *organizationResolver) CustomDomain(ctx context.Context, obj *types.Orga
return types.NewCustomDomain(domain, r.customDomainCname), nil
}
+// SamlConfigurations is the resolver for the samlConfigurations field.
+func (r *organizationResolver) SamlConfigurations(ctx context.Context, obj *types.Organization) ([]*types.SAMLConfiguration, error) {
+ tenantID := obj.ID.TenantID()
+
+ configs, err := r.authSvc.WithTenant(tenantID).GetSAMLConfigurationsByOrganizationID(ctx, obj.ID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load SAML configurations: %w", err)
+ }
+
+ result := make([]*types.SAMLConfiguration, len(configs))
+ for i, config := range configs {
+ result[i] = types.NewSAMLConfigurationWithURLs(
+ config,
+ r.samlSvc.GetEntityID(),
+ r.samlSvc.GetAcsURL(),
+ )
+ }
+
+ return result, nil
+}
+
// TotalCount is the resolver for the totalCount field.
func (r *peopleConnectionResolver) TotalCount(ctx context.Context, obj *types.PeopleConnection) (int, error) {
prb := r.ProboService(ctx, obj.ParentID.TenantID())
@@ -4727,6 +5020,41 @@ func (r *riskConnectionResolver) TotalCount(ctx context.Context, obj *types.Risk
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
}
+// Organization is the resolver for the organization field.
+func (r *sAMLConfigurationResolver) Organization(ctx context.Context, obj *types.SAMLConfiguration) (*types.Organization, error) {
+ tenantID := obj.ID.TenantID()
+ prb := r.ProboService(ctx, tenantID)
+
+ config, err := r.authSvc.WithTenant(tenantID).GetSAMLConfigurationByID(ctx, obj.ID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load SAML configuration: %w", err)
+ }
+
+ org, err := prb.Organizations.Get(ctx, config.OrganizationID)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load organization: %w", err)
+ }
+
+ return types.NewOrganization(org), nil
+}
+
+// SpMetadataURL is the resolver for the spMetadataUrl field.
+// Returns global Entity ID (same as spEntityId since metadata URL no longer needs config parameter)
+func (r *sAMLConfigurationResolver) SpMetadataURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error) {
+ return r.samlSvc.GetEntityID(), nil
+}
+
+// TestLoginURL is the resolver for the testLoginUrl field.
+func (r *sAMLConfigurationResolver) TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error) {
+ entityID := r.samlSvc.GetEntityID()
+ parts := strings.Split(entityID, "/auth/saml/metadata")
+ if len(parts) != 2 {
+ return "", fmt.Errorf("invalid entity ID format")
+ }
+
+ return fmt.Sprintf("%s/auth/saml/login/%s", parts[0], obj.ID), nil
+}
+
// Organization is the resolver for the organization field.
func (r *snapshotResolver) Organization(ctx context.Context, obj *types.Snapshot) (*types.Organization, error) {
prb := r.ProboService(ctx, obj.ID.TenantID())
@@ -5520,6 +5848,8 @@ func (r *viewerResolver) Organizations(ctx context.Context, obj *types.Viewer, f
panic(fmt.Errorf("failed to list organizations for user: %w", err))
}
+ // Show all organizations the user is a member of
+ // Authentication requirements will be enforced when switching to an organization
page := page.NewPage(organizations, cursor)
return types.NewOrganizationConnection(page), nil
@@ -5649,6 +5979,9 @@ func (r *Resolver) MeasureConnection() schema.MeasureConnectionResolver {
return &measureConnectionResolver{r}
}
+// Membership returns schema.MembershipResolver implementation.
+func (r *Resolver) Membership() schema.MembershipResolver { return &membershipResolver{r} }
+
// MembershipConnection returns schema.MembershipConnectionResolver implementation.
func (r *Resolver) MembershipConnection() schema.MembershipConnectionResolver {
return &membershipConnectionResolver{r}
@@ -5703,6 +6036,11 @@ func (r *Resolver) Risk() schema.RiskResolver { return &riskResolver{r} }
// RiskConnection returns schema.RiskConnectionResolver implementation.
func (r *Resolver) RiskConnection() schema.RiskConnectionResolver { return &riskConnectionResolver{r} }
+// SAMLConfiguration returns schema.SAMLConfigurationResolver implementation.
+func (r *Resolver) SAMLConfiguration() schema.SAMLConfigurationResolver {
+ return &sAMLConfigurationResolver{r}
+}
+
// Snapshot returns schema.SnapshotResolver implementation.
func (r *Resolver) Snapshot() schema.SnapshotResolver { return &snapshotResolver{r} }
@@ -5818,6 +6156,7 @@ type invitationResolver struct{ *Resolver }
type invitationConnectionResolver struct{ *Resolver }
type measureResolver struct{ *Resolver }
type measureConnectionResolver struct{ *Resolver }
+type membershipResolver struct{ *Resolver }
type membershipConnectionResolver struct{ *Resolver }
type mutationResolver struct{ *Resolver }
type nonconformityResolver struct{ *Resolver }
@@ -5832,6 +6171,7 @@ type queryResolver struct{ *Resolver }
type reportResolver struct{ *Resolver }
type riskResolver struct{ *Resolver }
type riskConnectionResolver struct{ *Resolver }
+type sAMLConfigurationResolver struct{ *Resolver }
type snapshotResolver struct{ *Resolver }
type snapshotConnectionResolver struct{ *Resolver }
type taskResolver struct{ *Resolver }
diff --git a/pkg/server/auth/accept_invitation_handler.go b/pkg/server/auth/accept_invitation_handler.go
new file mode 100644
index 000000000..a2f1b2ccb
--- /dev/null
+++ b/pkg/server/auth/accept_invitation_handler.go
@@ -0,0 +1,95 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ authsvc "github.com/getprobo/probo/pkg/auth"
+ "github.com/getprobo/probo/pkg/authz"
+ "github.com/getprobo/probo/pkg/gid"
+ "github.com/getprobo/probo/pkg/server/session"
+ "go.gearno.de/kit/httpserver"
+)
+
+type (
+ AcceptInvitationRequest struct {
+ InvitationID gid.GID `json:"invitationId"`
+ }
+
+ AcceptInvitationResponse struct {
+ InvitationID gid.GID `json:"invitationId"`
+ }
+)
+
+func AcceptInvitationHandler(authSvc *authsvc.Service, authzSvc *authz.Service, authCfg RoutesConfig) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ sessionAuthCfg := session.AuthConfig{
+ CookieName: authCfg.CookieName,
+ CookieSecret: authCfg.CookieSecret,
+ }
+
+ errorHandler := session.ErrorHandler{
+ OnCookieError: func(err error) {
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
+ },
+ OnParseError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
+ session.ClearCookie(w, authCfg)
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
+ },
+ OnSessionError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
+ session.ClearCookie(w, authCfg)
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("session expired"))
+ },
+ OnUserError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
+ session.ClearCookie(w, authCfg)
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("user not found"))
+ },
+ OnTenantError: func(err error) {
+ panic(fmt.Errorf("failed to list tenants for user: %w", err))
+ },
+ }
+
+ authResult := session.TryAuth(ctx, w, r, authSvc, authzSvc, sessionAuthCfg, errorHandler)
+ if authResult == nil {
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required"))
+ return
+ }
+
+ // Parse request body
+ var req AcceptInvitationRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("invalid request body"))
+ return
+ }
+
+ // Accept the invitation
+ _, err := authzSvc.AcceptInvitationByID(ctx, req.InvitationID, authResult.User.ID)
+ if err != nil {
+ httpserver.RenderError(w, http.StatusBadRequest, err)
+ return
+ }
+
+ response := AcceptInvitationResponse{
+ InvitationID: req.InvitationID,
+ }
+
+ httpserver.RenderJSON(w, http.StatusOK, response)
+ }
+}
diff --git a/pkg/server/auth/auth.go b/pkg/server/auth/auth.go
new file mode 100644
index 000000000..c4a5c0ca0
--- /dev/null
+++ b/pkg/server/auth/auth.go
@@ -0,0 +1,72 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "net/http"
+ "time"
+
+ authsvc "github.com/getprobo/probo/pkg/auth"
+ "github.com/getprobo/probo/pkg/authz"
+ "github.com/getprobo/probo/pkg/filemanager"
+ "github.com/go-chi/chi/v5"
+ "go.gearno.de/kit/log"
+ "go.gearno.de/kit/pg"
+)
+
+type Config struct {
+ Auth *authsvc.Service
+ Authz *authz.Service
+ SAML *authsvc.SAMLService
+ CookieName string
+ CookieDomain string
+ SessionDuration time.Duration
+ CookieSecret string
+ FileManager *filemanager.Service
+ PGClient *pg.Client
+ Logger *log.Logger
+}
+
+type Server struct {
+ router *chi.Mux
+}
+
+func NewServer(cfg Config) (*Server, error) {
+ router := chi.NewRouter()
+
+ MountRoutes(
+ router,
+ cfg.Auth,
+ cfg.Authz,
+ cfg.SAML,
+ RoutesConfig{
+ CookieName: cfg.CookieName,
+ CookieDomain: cfg.CookieDomain,
+ SessionDuration: cfg.SessionDuration,
+ CookieSecret: cfg.CookieSecret,
+ FileManager: cfg.FileManager,
+ PGClient: cfg.PGClient,
+ },
+ cfg.Logger,
+ )
+
+ return &Server{
+ router: router,
+ }, nil
+}
+
+func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ s.router.ServeHTTP(w, r)
+}
diff --git a/pkg/server/api/console/v1/forget_password_handler.go b/pkg/server/auth/forget_password_handler.go
similarity index 91%
rename from pkg/server/api/console/v1/forget_password_handler.go
rename to pkg/server/auth/forget_password_handler.go
index c227b7511..70ca8cef9 100644
--- a/pkg/server/api/console/v1/forget_password_handler.go
+++ b/pkg/server/auth/forget_password_handler.go
@@ -12,14 +12,14 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-package console_v1
+package auth
import (
"encoding/json"
"fmt"
"net/http"
- "github.com/getprobo/probo/pkg/auth"
+ authsvc "github.com/getprobo/probo/pkg/auth"
"go.gearno.de/kit/httpserver"
)
@@ -33,7 +33,7 @@ type (
}
)
-func ForgetPasswordHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
+func ForgetPasswordHandler(authSvc *authsvc.Service, authCfg RoutesConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req ForgetPasswordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
diff --git a/pkg/server/auth/list_invitations_handler.go b/pkg/server/auth/list_invitations_handler.go
new file mode 100644
index 000000000..a833a254b
--- /dev/null
+++ b/pkg/server/auth/list_invitations_handler.go
@@ -0,0 +1,203 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ authsvc "github.com/getprobo/probo/pkg/auth"
+ "github.com/getprobo/probo/pkg/authz"
+ "github.com/getprobo/probo/pkg/coredata"
+ "github.com/getprobo/probo/pkg/gid"
+ "github.com/getprobo/probo/pkg/page"
+ "github.com/getprobo/probo/pkg/server/session"
+ "go.gearno.de/kit/httpserver"
+ "go.gearno.de/kit/pg"
+)
+
+type (
+ ListInvitationsResponse struct {
+ Invitations []InvitationResponse `json:"invitations"`
+ }
+
+ InvitationResponse struct {
+ ID gid.GID `json:"id"`
+ Email string `json:"email"`
+ FullName string `json:"fullName"`
+ Role string `json:"role"`
+ ExpiresAt string `json:"expiresAt"`
+ AcceptedAt *string `json:"acceptedAt,omitempty"`
+ CreatedAt string `json:"createdAt"`
+ Organization OrganizationSummary `json:"organization"`
+ }
+
+ OrganizationSummary struct {
+ ID gid.GID `json:"id"`
+ Name string `json:"name"`
+ }
+)
+
+// loadOrganizationByID loads an organization by ID without tenant scope
+func loadOrganizationByID(
+ ctx context.Context,
+ conn pg.Conn,
+ orgID gid.GID,
+) (*coredata.Organization, error) {
+ query := `
+SELECT
+ id,
+ tenant_id,
+ name,
+ logo_file_id,
+ horizontal_logo_file_id,
+ description,
+ website_url,
+ email,
+ headquarter_address,
+ custom_domain_id,
+ created_at,
+ updated_at
+FROM
+ authz_organizations
+WHERE
+ id = $1
+`
+
+ row := conn.QueryRow(ctx, query, orgID)
+
+ var org coredata.Organization
+ err := row.Scan(
+ &org.ID,
+ &org.TenantID,
+ &org.Name,
+ &org.LogoFileID,
+ &org.HorizontalLogoFileID,
+ &org.Description,
+ &org.WebsiteURL,
+ &org.Email,
+ &org.HeadquarterAddress,
+ &org.CustomDomainID,
+ &org.CreatedAt,
+ &org.UpdatedAt,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("cannot load organization: %w", err)
+ }
+
+ return &org, nil
+}
+
+func ListInvitationsHandler(authSvc *authsvc.Service, authzSvc *authz.Service, authCfg RoutesConfig) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ sessionAuthCfg := session.AuthConfig{
+ CookieName: authCfg.CookieName,
+ CookieSecret: authCfg.CookieSecret,
+ }
+
+ errorHandler := session.ErrorHandler{
+ OnCookieError: func(err error) {
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
+ },
+ OnParseError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
+ session.ClearCookie(w, authCfg)
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
+ },
+ OnSessionError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
+ session.ClearCookie(w, authCfg)
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("session expired"))
+ },
+ OnUserError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
+ session.ClearCookie(w, authCfg)
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("user not found"))
+ },
+ OnTenantError: func(err error) {
+ panic(fmt.Errorf("failed to list tenants for user: %w", err))
+ },
+ }
+
+ authResult := session.TryAuth(ctx, w, r, authSvc, authzSvc, sessionAuthCfg, errorHandler)
+ if authResult == nil {
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required"))
+ return
+ }
+
+ // Get pending invitations for the user
+ cursor := page.NewCursor(
+ 1000,
+ nil,
+ page.Head,
+ page.OrderBy[coredata.InvitationOrderField]{
+ Field: coredata.InvitationOrderFieldCreatedAt,
+ Direction: page.OrderDirectionDesc,
+ },
+ )
+
+ invitationFilter := coredata.NewInvitationFilter([]coredata.InvitationStatus{coredata.InvitationStatusPending})
+
+ invitationsPage, err := authzSvc.GetUserInvitations(ctx, authResult.User.EmailAddress, cursor, invitationFilter)
+ if err != nil {
+ panic(fmt.Errorf("failed to list invitations for user: %w", err))
+ }
+
+ // Build response
+ response := ListInvitationsResponse{
+ Invitations: make([]InvitationResponse, 0, len(invitationsPage.Data)),
+ }
+
+ // Load organization data for each invitation
+ err = authCfg.PGClient.WithConn(ctx, func(conn pg.Conn) error {
+ for _, invitation := range invitationsPage.Data {
+ invitationResp := InvitationResponse{
+ ID: invitation.ID,
+ Email: invitation.Email,
+ FullName: invitation.FullName,
+ Role: invitation.Role,
+ ExpiresAt: invitation.ExpiresAt.Format("2006-01-02T15:04:05Z07:00"),
+ CreatedAt: invitation.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
+ }
+
+ if invitation.AcceptedAt != nil {
+ acceptedAtStr := invitation.AcceptedAt.Format("2006-01-02T15:04:05Z07:00")
+ invitationResp.AcceptedAt = &acceptedAtStr
+ }
+
+ // Load organization details
+ org, err := loadOrganizationByID(ctx, conn, invitation.OrganizationID)
+ if err != nil {
+ // Log error but continue - organization might have been deleted
+ return nil
+ }
+
+ invitationResp.Organization = OrganizationSummary{
+ ID: org.ID,
+ Name: org.Name,
+ }
+
+ response.Invitations = append(response.Invitations, invitationResp)
+ }
+
+ return nil
+ })
+ if err != nil {
+ panic(fmt.Errorf("failed to load organization details: %w", err))
+ }
+
+ httpserver.RenderJSON(w, http.StatusOK, response)
+ }
+}
diff --git a/pkg/server/auth/list_organizations_handler.go b/pkg/server/auth/list_organizations_handler.go
new file mode 100644
index 000000000..0b1ba96d2
--- /dev/null
+++ b/pkg/server/auth/list_organizations_handler.go
@@ -0,0 +1,201 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "time"
+
+ authsvc "github.com/getprobo/probo/pkg/auth"
+ "github.com/getprobo/probo/pkg/authz"
+ "github.com/getprobo/probo/pkg/coredata"
+ "github.com/getprobo/probo/pkg/filemanager"
+ "github.com/getprobo/probo/pkg/gid"
+ "github.com/getprobo/probo/pkg/server/session"
+ "go.gearno.de/kit/httpserver"
+ "go.gearno.de/kit/pg"
+)
+
+type (
+ AuthenticationStatus string
+
+ ListOrganizationsResponse struct {
+ Organizations []OrganizationResponse `json:"organizations"`
+ }
+
+ OrganizationResponse struct {
+ ID gid.GID `json:"id"`
+ Name string `json:"name"`
+ LogoURL *string `json:"logoUrl,omitempty"`
+ AuthenticationMethod string `json:"authenticationMethod"` // "password", "saml", or "any"
+ AuthStatus AuthenticationStatus `json:"authStatus"` // "authenticated", "unauthenticated", "expired"
+ LoginURL string `json:"loginUrl"` // URL to login (SAML or password login page)
+ }
+)
+
+const (
+ AuthStatusAuthenticated AuthenticationStatus = "authenticated"
+ AuthStatusUnauthenticated AuthenticationStatus = "unauthenticated"
+ AuthStatusExpired AuthenticationStatus = "expired"
+)
+
+// generateLogoURL generates a presigned URL for an organization's logo
+func generateLogoURL(
+ ctx context.Context,
+ fileManager *filemanager.Service,
+ conn pg.Conn,
+ logoFileID *gid.GID,
+) (*string, error) {
+ if logoFileID == nil {
+ return nil, nil
+ }
+
+ var file coredata.File
+ // Load file without scope since we're in auth context (cross-tenant)
+ q := `SELECT bucket_name, file_key, file_name, mime_type, file_size FROM files WHERE id = $1`
+ err := conn.QueryRow(ctx, q, logoFileID).Scan(
+ &file.BucketName,
+ &file.FileKey,
+ &file.FileName,
+ &file.MimeType,
+ &file.FileSize,
+ )
+ if err != nil {
+ return nil, fmt.Errorf("cannot load file: %w", err)
+ }
+
+ presignedURL, err := fileManager.GenerateFileUrl(ctx, &file, 1*time.Hour)
+ if err != nil {
+ return nil, fmt.Errorf("cannot generate file URL: %w", err)
+ }
+
+ return &presignedURL, nil
+}
+
+func ListOrganizationsHandler(authSvc *authsvc.Service, authzSvc *authz.Service, authCfg RoutesConfig) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ sessionAuthCfg := session.AuthConfig{
+ CookieName: authCfg.CookieName,
+ CookieSecret: authCfg.CookieSecret,
+ }
+
+ errorHandler := session.ErrorHandler{
+ OnCookieError: func(err error) {
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
+ },
+ OnParseError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
+ session.ClearCookie(w, authCfg)
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
+ },
+ OnSessionError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
+ session.ClearCookie(w, authCfg)
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("session expired"))
+ },
+ OnUserError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
+ session.ClearCookie(w, authCfg)
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("user not found"))
+ },
+ OnTenantError: func(err error) {
+ panic(fmt.Errorf("failed to list tenants for user: %w", err))
+ },
+ }
+
+ authResult := session.TryAuth(ctx, w, r, authSvc, authzSvc, sessionAuthCfg, errorHandler)
+ if authResult == nil {
+ httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required"))
+ return
+ }
+
+ // Get all organizations for the user (without filtering by authentication state)
+ organizations, err := authzSvc.GetAllUserOrganizations(ctx, authResult.User.ID)
+ if err != nil {
+ panic(fmt.Errorf("failed to list organizations for user: %w", err))
+ }
+
+ // Build response with authentication requirements for each organization
+ response := ListOrganizationsResponse{
+ Organizations: make([]OrganizationResponse, 0, len(organizations)),
+ }
+
+ for _, org := range organizations {
+ orgResponse := OrganizationResponse{
+ ID: org.ID,
+ Name: org.Name,
+ }
+
+ // Generate logo URL if available
+ if authCfg.FileManager != nil && authCfg.PGClient != nil {
+ err := authCfg.PGClient.WithConn(ctx, func(conn pg.Conn) error {
+ logoURL, err := generateLogoURL(ctx, authCfg.FileManager, conn, org.LogoFileID)
+ if err != nil {
+ // Log error but don't fail the request
+ return nil
+ }
+ orgResponse.LogoURL = logoURL
+ return nil
+ })
+ if err != nil {
+ // Log error but continue
+ }
+ }
+
+ // Check authentication requirements for this organization
+ err := authSvc.CheckOrganizationAccess(ctx, authResult.User, org.ID, authResult.Session)
+ if err != nil {
+ // User needs additional authentication
+ var errSAMLRequired authsvc.ErrSAMLAuthRequired
+ if errors.As(err, &errSAMLRequired) {
+ orgResponse.AuthenticationMethod = "saml"
+ orgResponse.AuthStatus = AuthStatusUnauthenticated
+ orgResponse.LoginURL = fmt.Sprintf("/auth/saml/login/%s", errSAMLRequired.ConfigID)
+ } else {
+ orgResponse.AuthenticationMethod = "password"
+ orgResponse.AuthStatus = AuthStatusUnauthenticated
+ orgResponse.LoginURL = "/authentication/login?method=password"
+ }
+ } else {
+ // User has proper authentication
+ orgResponse.AuthStatus = AuthStatusAuthenticated
+
+ // Determine which auth method they used
+ if authResult.Session.Data.PasswordAuthenticated {
+ orgResponse.AuthenticationMethod = "password"
+ orgResponse.LoginURL = "/authentication/login?method=password"
+ } else if len(authResult.Session.Data.SAMLAuthenticatedOrgs) > 0 {
+ // Find SAML config for this org
+ orgResponse.AuthenticationMethod = "saml"
+ // Try to find the SAML config ID for login URL
+ if samlInfo, ok := authResult.Session.Data.SAMLAuthenticatedOrgs[org.ID.String()]; ok {
+ orgResponse.LoginURL = fmt.Sprintf("/auth/saml/login/%s", samlInfo.SAMLConfigID)
+ } else {
+ orgResponse.LoginURL = "/authentication/login?method=password"
+ }
+ } else {
+ orgResponse.AuthenticationMethod = "any"
+ orgResponse.LoginURL = "/authentication/login?method=password"
+ }
+ }
+
+ response.Organizations = append(response.Organizations, orgResponse)
+ }
+
+ httpserver.RenderJSON(w, http.StatusOK, response)
+ }
+}
diff --git a/pkg/server/api/console/v1/reset_password_handler.go b/pkg/server/auth/reset_password_handler.go
similarity index 87%
rename from pkg/server/api/console/v1/reset_password_handler.go
rename to pkg/server/auth/reset_password_handler.go
index 4a85e7c42..32a03f14e 100644
--- a/pkg/server/api/console/v1/reset_password_handler.go
+++ b/pkg/server/auth/reset_password_handler.go
@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-package console_v1
+package auth
import (
"encoding/json"
@@ -21,7 +21,7 @@ import (
"errors"
- "github.com/getprobo/probo/pkg/auth"
+ authsvc "github.com/getprobo/probo/pkg/auth"
"go.gearno.de/kit/httpserver"
)
@@ -36,7 +36,7 @@ type (
}
)
-func ResetPasswordHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
+func ResetPasswordHandler(authSvc *authsvc.Service, authCfg RoutesConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req ResetPasswordRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -46,8 +46,8 @@ func ResetPasswordHandler(authSvc *auth.Service, authCfg AuthConfig) http.Handle
err := authSvc.ResetPassword(r.Context(), req.Token, req.Password)
if err != nil {
- var invalidPasswordErr *auth.ErrInvalidPassword
- var invalidTokenErr *auth.ErrInvalidTokenType
+ var invalidPasswordErr *authsvc.ErrInvalidPassword
+ var invalidTokenErr *authsvc.ErrInvalidTokenType
if errors.As(err, &invalidPasswordErr) {
httpserver.RenderError(w, http.StatusBadRequest, err)
diff --git a/pkg/server/auth/router.go b/pkg/server/auth/router.go
new file mode 100644
index 000000000..f2981b047
--- /dev/null
+++ b/pkg/server/auth/router.go
@@ -0,0 +1,60 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "time"
+
+ authsvc "github.com/getprobo/probo/pkg/auth"
+ "github.com/getprobo/probo/pkg/authz"
+ "github.com/getprobo/probo/pkg/filemanager"
+ "github.com/go-chi/chi/v5"
+ "go.gearno.de/kit/log"
+ "go.gearno.de/kit/pg"
+)
+
+type RoutesConfig struct {
+ CookieName string
+ CookieDomain string
+ SessionDuration time.Duration
+ CookieSecret string
+ FileManager *filemanager.Service
+ PGClient *pg.Client
+}
+
+func MountRoutes(
+ r chi.Router,
+ authSvc *authsvc.Service,
+ authzSvc *authz.Service,
+ samlSvc *authsvc.SAMLService,
+ authCfg RoutesConfig,
+ logger *log.Logger,
+) {
+ r.Post("/register", SignUpHandler(authSvc, authCfg))
+ r.Post("/login", SignInHandler(authSvc, authCfg))
+ r.Delete("/logout", SignOutHandler(authSvc, authCfg))
+ r.Post("/signup-from-invitation", SignupFromInvitationHandler(authSvc, authCfg))
+ r.Post("/forget-password", ForgetPasswordHandler(authSvc, authCfg))
+ r.Post("/reset-password", ResetPasswordHandler(authSvc, authCfg))
+ r.Post("/check-sso", SAMLCheckSSOHandler(authSvc, logger))
+ r.Get("/organizations", ListOrganizationsHandler(authSvc, authzSvc, authCfg))
+ r.Get("/invitations", ListInvitationsHandler(authSvc, authzSvc, authCfg))
+ r.Post("/invitations/accept", AcceptInvitationHandler(authSvc, authzSvc, authCfg))
+
+ // SAML routes
+ r.Get("/saml/login/{samlConfigID}", SAMLLoginHandler(samlSvc, authSvc, logger))
+ r.Post("/saml/consume", SAMLACSHandler(samlSvc, authSvc, authzSvc, authCfg, logger))
+ r.Get("/saml/metadata", SAMLMetadataHandler(samlSvc))
+}
diff --git a/pkg/server/auth/saml_acs_handler.go b/pkg/server/auth/saml_acs_handler.go
new file mode 100644
index 000000000..72511aa2f
--- /dev/null
+++ b/pkg/server/auth/saml_acs_handler.go
@@ -0,0 +1,131 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ authsvc "github.com/getprobo/probo/pkg/auth"
+ "github.com/getprobo/probo/pkg/authz"
+ "github.com/getprobo/probo/pkg/coredata"
+ "github.com/getprobo/probo/pkg/gid"
+ "github.com/getprobo/probo/pkg/securecookie"
+ "go.gearno.de/kit/log"
+)
+
+func getSessionIDFromCookie(r *http.Request, authCfg RoutesConfig) (gid.GID, error) {
+ cookieValue, err := securecookie.Get(r, securecookie.DefaultConfig(
+ authCfg.CookieName,
+ authCfg.CookieSecret,
+ ))
+ if err != nil {
+ return gid.GID{}, err
+ }
+
+ return gid.ParseGID(cookieValue)
+}
+
+func SAMLACSHandler(samlSvc *authsvc.SAMLService, authSvc *authsvc.Service, authzSvc *authz.Service, authCfg RoutesConfig, logger *log.Logger) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ if err := r.ParseForm(); err != nil {
+ logger.ErrorCtx(ctx, "failed to parse form", log.Error(err))
+ http.Error(w, "failed to parse form", http.StatusBadRequest)
+ return
+ }
+
+ if r.FormValue("SAMLResponse") == "" {
+ logger.WarnCtx(ctx, "missing SAMLResponse")
+ http.Error(w, "missing SAMLResponse", http.StatusBadRequest)
+ return
+ }
+
+ if r.FormValue("RelayState") == "" {
+ logger.WarnCtx(ctx, "missing RelayState")
+ http.Error(w, "missing RelayState", http.StatusBadRequest)
+ return
+ }
+
+ userInfo, err := samlSvc.HandleSAMLAssertion(ctx, r)
+ if err != nil {
+ logger.ErrorCtx(ctx, "SAML authentication failed", log.Error(err))
+ http.Error(w, "SAML authentication failed", http.StatusUnauthorized)
+ return
+ }
+
+ user, err := authSvc.CreateOrGetSAMLUser(ctx, userInfo.Email, userInfo.FullName, userInfo.SAMLSubject)
+ if err != nil {
+ logger.ErrorCtx(ctx, "cannot create or get SAML user", log.Error(err), log.String("email", userInfo.Email))
+ http.Error(w, "failed to create user", http.StatusInternalServerError)
+ return
+ }
+
+ err = authzSvc.EnsureSAMLMembership(ctx, userInfo.TenantID, user.ID, userInfo.OrganizationID, userInfo.Role)
+ if err != nil {
+ logger.ErrorCtx(ctx, "cannot ensure membership", log.Error(err), log.String("user_id", user.ID.String()), log.String("org_id", userInfo.OrganizationID.String()))
+ http.Error(w, "failed to create membership", http.StatusInternalServerError)
+ return
+ }
+
+ var session *coredata.Session
+ if existingSessionID, err := getSessionIDFromCookie(r, authCfg); err == nil {
+ if existingSession, err := authSvc.GetSession(ctx, existingSessionID); err == nil && existingSession.UserID == user.ID {
+ session = existingSession
+ }
+ }
+
+ if session == nil {
+ session, err = authSvc.CreateSessionForUser(ctx, user.ID, authCfg.SessionDuration)
+ if err != nil {
+ logger.ErrorCtx(ctx, "cannot create session", log.Error(err), log.String("user_id", user.ID.String()))
+ http.Error(w, "failed to create session", http.StatusInternalServerError)
+ return
+ }
+ }
+
+ if session.Data.SAMLAuthenticatedOrgs == nil {
+ session.Data.SAMLAuthenticatedOrgs = make(map[string]coredata.SAMLAuthInfo)
+ }
+ session.Data.SAMLAuthenticatedOrgs[userInfo.OrganizationID.String()] = coredata.SAMLAuthInfo{
+ AuthenticatedAt: time.Now(),
+ SAMLConfigID: userInfo.SAMLConfigID,
+ SAMLSubject: userInfo.SAMLSubject,
+ }
+
+ err = authSvc.UpdateSessionData(ctx, session.ID, session.Data)
+ if err != nil {
+ logger.ErrorCtx(ctx, "cannot update session data", log.Error(err), log.String("session_id", session.ID.String()))
+ http.Error(w, "failed to update session", http.StatusInternalServerError)
+ return
+ }
+
+ securecookie.Set(
+ w,
+ securecookie.DefaultConfig(
+ authCfg.CookieName,
+ authCfg.CookieSecret,
+ ),
+ session.ID.String(),
+ )
+
+ logger.InfoCtx(ctx, "SAML login successful", log.String("user_id", user.ID.String()), log.String("org_id", userInfo.OrganizationID.String()))
+
+ redirectURL := fmt.Sprintf("/organizations/%s", userInfo.OrganizationID)
+ http.Redirect(w, r, redirectURL, http.StatusFound)
+ }
+}
diff --git a/pkg/server/auth/saml_check_sso_handler.go b/pkg/server/auth/saml_check_sso_handler.go
new file mode 100644
index 000000000..16118790a
--- /dev/null
+++ b/pkg/server/auth/saml_check_sso_handler.go
@@ -0,0 +1,90 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+
+ authsvc "github.com/getprobo/probo/pkg/auth"
+ "go.gearno.de/kit/httpserver"
+ "go.gearno.de/kit/log"
+)
+
+type (
+ CheckSSORequest struct {
+ Email string `json:"email"`
+ }
+
+ CheckSSOResponse struct {
+ SSOAvailable bool `json:"ssoAvailable"`
+ SAMLConfigID *string `json:"samlConfigId,omitempty"`
+ OrganizationID *string `json:"organizationId,omitempty"`
+ EnforcementPolicy *string `json:"enforcementPolicy,omitempty"`
+ }
+)
+
+func SAMLCheckSSOHandler(authSvc *authsvc.Service, logger *log.Logger) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ var req CheckSSORequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
+ return
+ }
+
+ if req.Email == "" {
+ httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("email is required"))
+ return
+ }
+
+ configs, err := authSvc.CheckSSOAvailabilityByEmail(ctx, req.Email)
+ if err != nil {
+ logger.ErrorCtx(ctx, "cannot check SSO availability", log.Error(err), log.String("email", req.Email))
+ httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("cannot check SSO availability"))
+ return
+ }
+
+ // No SAML configs found for this domain
+ if len(configs) == 0 {
+ httpserver.RenderJSON(w, http.StatusOK, CheckSSOResponse{
+ SSOAvailable: false,
+ })
+ return
+ }
+
+ // Multiple SAML configs found - ambiguous, user must use organization-specific SSO URL
+ if len(configs) > 1 {
+ logger.WarnCtx(ctx, "multiple SAML configurations found for domain", log.String("email", req.Email), log.Int("count", len(configs)))
+ httpserver.RenderError(w, http.StatusConflict, fmt.Errorf("multiple SSO configurations found for this domain. Please use your organization-specific SSO login URL"))
+ return
+ }
+
+ // Single SAML config found - return it
+ config := configs[0]
+ configIDStr := config.ID.String()
+ orgIDStr := config.OrganizationID.String()
+ enforcementPolicy := string(config.EnforcementPolicy)
+
+ httpserver.RenderJSON(w, http.StatusOK, CheckSSOResponse{
+ SSOAvailable: true,
+ SAMLConfigID: &configIDStr,
+ OrganizationID: &orgIDStr,
+ EnforcementPolicy: &enforcementPolicy,
+ })
+ }
+}
diff --git a/pkg/server/auth/saml_login_handler.go b/pkg/server/auth/saml_login_handler.go
new file mode 100644
index 000000000..c027cf560
--- /dev/null
+++ b/pkg/server/auth/saml_login_handler.go
@@ -0,0 +1,65 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "fmt"
+ "net/http"
+
+ authsvc "github.com/getprobo/probo/pkg/auth"
+ "github.com/getprobo/probo/pkg/gid"
+ "github.com/go-chi/chi/v5"
+ "go.gearno.de/kit/log"
+)
+
+func SAMLLoginHandler(samlSvc *authsvc.SAMLService, authSvc *authsvc.Service, logger *log.Logger) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ ctx := r.Context()
+
+ samlConfigIDStr := chi.URLParam(r, "samlConfigID")
+ if samlConfigIDStr == "" {
+ logger.WarnCtx(ctx, "missing SAML config ID in URL")
+ http.Error(w, "missing SAML config ID", http.StatusBadRequest)
+ return
+ }
+
+ samlConfigID, err := gid.ParseGID(samlConfigIDStr)
+ if err != nil {
+ logger.ErrorCtx(ctx, "invalid SAML config ID", log.Error(err), log.String("saml_config_id", samlConfigIDStr))
+ http.Error(w, "invalid SAML config ID", http.StatusBadRequest)
+ return
+ }
+
+ tenantID := samlConfigID.TenantID()
+
+ config, err := authSvc.WithTenant(tenantID).GetSAMLConfigurationByID(ctx, samlConfigID)
+ if err != nil {
+ logger.ErrorCtx(ctx, "cannot load SAML configuration", log.Error(err), log.String("saml_config_id", samlConfigID.String()))
+ http.Error(w, "SAML configuration not found", http.StatusNotFound)
+ return
+ }
+
+ redirectURL, err := samlSvc.InitiateSAMLLogin(ctx, config.OrganizationID, tenantID, config.EmailDomain)
+ if err != nil {
+ logger.ErrorCtx(ctx, "cannot initiate SAML login", log.Error(err), log.String("saml_config_id", samlConfigID.String()), log.String("org_id", config.OrganizationID.String()), log.String("email_domain", config.EmailDomain))
+ http.Error(w, fmt.Sprintf("SAML login failed: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ logger.InfoCtx(ctx, "SAML login initiated", log.String("saml_config_id", samlConfigID.String()), log.String("org_id", config.OrganizationID.String()), log.String("email_domain", config.EmailDomain))
+
+ http.Redirect(w, r, redirectURL, http.StatusFound)
+ }
+}
diff --git a/pkg/server/auth/saml_metadata_handler.go b/pkg/server/auth/saml_metadata_handler.go
new file mode 100644
index 000000000..730a77216
--- /dev/null
+++ b/pkg/server/auth/saml_metadata_handler.go
@@ -0,0 +1,38 @@
+// Copyright (c) 2025 Probo Inc .
+//
+// 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 auth
+
+import (
+ "fmt"
+ "net/http"
+
+ authsvc "github.com/getprobo/probo/pkg/auth"
+)
+
+// SAMLMetadataHandler returns an HTTP handler that serves the SAML Service Provider metadata XML
+// Uses global SP certificate configured at service startup
+func SAMLMetadataHandler(samlSvc *authsvc.SAMLService) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ metadataXML, err := samlSvc.GenerateMetadata()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to generate metadata: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/samlmetadata+xml")
+ w.WriteHeader(http.StatusOK)
+ w.Write(metadataXML)
+ }
+}
diff --git a/pkg/server/api/console/v1/sign_in_handler.go b/pkg/server/auth/sign_in_handler.go
similarity index 78%
rename from pkg/server/api/console/v1/sign_in_handler.go
rename to pkg/server/auth/sign_in_handler.go
index f0e870a9d..586220eb9 100644
--- a/pkg/server/api/console/v1/sign_in_handler.go
+++ b/pkg/server/auth/sign_in_handler.go
@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-package console_v1
+package auth
import (
"encoding/json"
@@ -21,9 +21,10 @@ import (
"net/http"
"time"
+ authsvc "github.com/getprobo/probo/pkg/auth"
+ "github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/securecookie"
- "github.com/getprobo/probo/pkg/auth"
"go.gearno.de/kit/httpserver"
)
@@ -46,7 +47,7 @@ type (
}
)
-func SignInHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
+func SignInHandler(authSvc *authsvc.Service, authCfg RoutesConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req SignInRequest
@@ -55,9 +56,16 @@ func SignInHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
return
}
- session, user, err := authSvc.SignIn(r.Context(), req.Email, req.Password)
+ var existingSession *coredata.Session
+ if existingSessionID, err := getSessionIDFromCookie(r, authCfg); err == nil {
+ if session, err := authSvc.GetSession(r.Context(), existingSessionID); err == nil {
+ existingSession = session
+ }
+ }
+
+ session, user, err := authSvc.SignInWithExistingSession(r.Context(), req.Email, req.Password, existingSession)
if err != nil {
- var ErrInvalidCredentials *auth.ErrInvalidCredentials
+ var ErrInvalidCredentials *authsvc.ErrInvalidCredentials
if errors.As(err, &ErrInvalidCredentials) {
httpserver.RenderError(w, http.StatusUnauthorized, err)
return
diff --git a/pkg/server/api/console/v1/sign_out_handler.go b/pkg/server/auth/sign_out_handler.go
similarity index 91%
rename from pkg/server/api/console/v1/sign_out_handler.go
rename to pkg/server/auth/sign_out_handler.go
index 5e9302f27..249f5f084 100644
--- a/pkg/server/api/console/v1/sign_out_handler.go
+++ b/pkg/server/auth/sign_out_handler.go
@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-package console_v1
+package auth
import (
"fmt"
@@ -20,11 +20,11 @@ import (
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/securecookie"
- "github.com/getprobo/probo/pkg/auth"
+ authsvc "github.com/getprobo/probo/pkg/auth"
"go.gearno.de/kit/httpserver"
)
-func SignOutHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
+func SignOutHandler(authSvc *authsvc.Service, authCfg RoutesConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sessionID, err := securecookie.Get(r, securecookie.DefaultConfig(
diff --git a/pkg/server/api/console/v1/sign_up_handler.go b/pkg/server/auth/sign_up_handler.go
similarity index 90%
rename from pkg/server/api/console/v1/sign_up_handler.go
rename to pkg/server/auth/sign_up_handler.go
index d70696082..a83516caa 100644
--- a/pkg/server/api/console/v1/sign_up_handler.go
+++ b/pkg/server/auth/sign_up_handler.go
@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-package console_v1
+package auth
import (
"encoding/json"
@@ -20,7 +20,7 @@ import (
"fmt"
"net/http"
- "github.com/getprobo/probo/pkg/auth"
+ authsvc "github.com/getprobo/probo/pkg/auth"
"github.com/getprobo/probo/pkg/securecookie"
"go.gearno.de/kit/httpserver"
)
@@ -37,7 +37,7 @@ type (
}
)
-func SignUpHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
+func SignUpHandler(authSvc *authsvc.Service, authCfg RoutesConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req SignUpRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -52,13 +52,13 @@ func SignUpHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
req.FullName,
)
if err != nil {
- var errUserAlreadyExists *auth.ErrUserAlreadyExists
+ var errUserAlreadyExists *authsvc.ErrUserAlreadyExists
if errors.As(err, &errUserAlreadyExists) {
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot register user: %w", err))
return
}
- var errSignupDisabled *auth.ErrSignupDisabled
+ var errSignupDisabled *authsvc.ErrSignupDisabled
if errors.As(err, &errSignupDisabled) {
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot register user: %w", err))
return
diff --git a/pkg/server/api/console/v1/signup_from_invitation_handler.go b/pkg/server/auth/signup_from_invitation_handler.go
similarity index 92%
rename from pkg/server/api/console/v1/signup_from_invitation_handler.go
rename to pkg/server/auth/signup_from_invitation_handler.go
index 9d1c11301..4adb7dab0 100644
--- a/pkg/server/api/console/v1/signup_from_invitation_handler.go
+++ b/pkg/server/auth/signup_from_invitation_handler.go
@@ -12,14 +12,14 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
-package console_v1
+package auth
import (
"encoding/json"
"fmt"
"net/http"
- "github.com/getprobo/probo/pkg/auth"
+ authsvc "github.com/getprobo/probo/pkg/auth"
"github.com/getprobo/probo/pkg/securecookie"
"go.gearno.de/kit/httpserver"
)
@@ -35,7 +35,7 @@ type (
}
)
-func SignupFromInvitationHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
+func SignupFromInvitationHandler(authSvc *authsvc.Service, authCfg RoutesConfig) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req SignupFromInvitationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
diff --git a/pkg/server/graphql/recovery.go b/pkg/server/graphql/recovery.go
index 462388353..fc38729f8 100644
--- a/pkg/server/graphql/recovery.go
+++ b/pkg/server/graphql/recovery.go
@@ -19,13 +19,53 @@ import (
"errors"
"runtime/debug"
+ "github.com/getprobo/probo/pkg/auth"
+ "github.com/vektah/gqlparser/v2/gqlerror"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
)
func RecoverFunc(ctx context.Context, err any) error {
+ if gqlErr, ok := err.(*gqlerror.Error); ok {
+ return gqlErr
+ }
+
+ var errSAMLRequired auth.ErrSAMLAuthRequired
+ if errors.As(asError(err), &errSAMLRequired) {
+ return &gqlerror.Error{
+ Message: "Additional authentication required to access this organization",
+ Extensions: map[string]any{
+ "code": "AUTHENTICATION_REQUIRED",
+ "requiresSaml": true,
+ "redirectUrl": errSAMLRequired.RedirectURL,
+ "samlConfigId": errSAMLRequired.ConfigID.String(),
+ "organizationId": errSAMLRequired.OrganizationID.String(),
+ },
+ }
+ }
+
+ var errPasswordRequired auth.ErrPasswordAuthRequired
+ if errors.As(asError(err), &errPasswordRequired) {
+ return &gqlerror.Error{
+ Message: "Additional authentication required to access this organization",
+ Extensions: map[string]any{
+ "code": "AUTHENTICATION_REQUIRED",
+ "requiresSaml": false,
+ "redirectUrl": errPasswordRequired.RedirectURL,
+ "organizationId": errPasswordRequired.OrganizationID.String(),
+ },
+ }
+ }
+
logger := httpserver.LoggerFromContext(ctx)
logger.Error("resolver panic", log.Any("error", err), log.Any("stack", string(debug.Stack())))
return errors.New("internal server error")
}
+
+func asError(err any) error {
+ if e, ok := err.(error); ok {
+ return e
+ }
+ return errors.New("unknown panic")
+}
diff --git a/pkg/server/server.go b/pkg/server/server.go
index f81558db4..c6bc0bd80 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -24,10 +24,12 @@ import (
"github.com/getprobo/probo/pkg/auth"
"github.com/getprobo/probo/pkg/authz"
"github.com/getprobo/probo/pkg/connector"
+ "github.com/getprobo/probo/pkg/filemanager"
"github.com/getprobo/probo/pkg/gid"
"github.com/getprobo/probo/pkg/probo"
"github.com/getprobo/probo/pkg/saferedirect"
"github.com/getprobo/probo/pkg/server/api"
+ auth_server "github.com/getprobo/probo/pkg/server/auth"
trust_v1 "github.com/getprobo/probo/pkg/server/api/trust/v1"
"github.com/getprobo/probo/pkg/server/trust"
"github.com/getprobo/probo/pkg/server/web"
@@ -35,6 +37,7 @@ import (
"github.com/go-chi/chi/v5"
"go.gearno.de/kit/httpserver"
"go.gearno.de/kit/log"
+ "go.gearno.de/kit/pg"
)
type Config struct {
@@ -44,12 +47,15 @@ type Config struct {
Auth *auth.Service
Authz *authz.Service
Trust *trust_pkg.Service
+ SAML *auth.SAMLService
ConsoleAuth api.ConsoleAuthConfig
TrustAuth api.TrustAuthConfig
ConnectorRegistry *connector.ConnectorRegistry
Agent *agents.Agent
SafeRedirect *saferedirect.SafeRedirect
CustomDomainCname string
+ FileManager *filemanager.Service
+ PGClient *pg.Client
Logger *log.Logger
}
@@ -57,6 +63,7 @@ type Server struct {
apiServer *api.Server
webServer *web.Server
trustServer *trust.Server
+ authServer *auth_server.Server
router *chi.Mux
extraHeaderFields map[string]string
proboService *probo.Service
@@ -70,6 +77,7 @@ func NewServer(cfg Config) (*Server, error) {
Auth: cfg.Auth,
Authz: cfg.Authz,
Trust: cfg.Trust,
+ SAML: cfg.SAML,
ConsoleAuth: cfg.ConsoleAuth,
TrustAuth: cfg.TrustAuth,
ConnectorRegistry: cfg.ConnectorRegistry,
@@ -92,12 +100,29 @@ func NewServer(cfg Config) (*Server, error) {
return nil, err
}
+ authServer, err := auth_server.NewServer(auth_server.Config{
+ Auth: cfg.Auth,
+ Authz: cfg.Authz,
+ SAML: cfg.SAML,
+ CookieName: cfg.ConsoleAuth.CookieName,
+ CookieDomain: cfg.ConsoleAuth.CookieDomain,
+ SessionDuration: cfg.ConsoleAuth.SessionDuration,
+ CookieSecret: cfg.ConsoleAuth.CookieSecret,
+ FileManager: cfg.FileManager,
+ PGClient: cfg.PGClient,
+ Logger: cfg.Logger.Named("auth"),
+ })
+ if err != nil {
+ return nil, err
+ }
+
router := chi.NewRouter()
server := &Server{
apiServer: apiServer,
webServer: webServer,
trustServer: trustServer,
+ authServer: authServer,
router: router,
extraHeaderFields: cfg.ExtraHeaderFields,
proboService: cfg.Probo,
@@ -111,6 +136,7 @@ func NewServer(cfg Config) (*Server, error) {
func (s *Server) setupRoutes() {
s.router.Mount("/api", s.apiServer)
+ s.router.Mount("/auth", s.authServer)
s.router.Route("/trust/{slugOrId}", func(r chi.Router) {
r.Use(s.loadTrustCenterBySlugOrID)
diff --git a/pkg/server/session/session.go b/pkg/server/session/session.go
index 5da667e28..a3ba8be30 100644
--- a/pkg/server/session/session.go
+++ b/pkg/server/session/session.go
@@ -32,9 +32,10 @@ type AuthConfig struct {
}
type AuthResult struct {
- Session *coredata.Session
- User *coredata.User
- TenantIDs []gid.TenantID
+ Session *coredata.Session
+ User *coredata.User
+ TenantIDs []gid.TenantID
+ AuthErrors map[gid.TenantID]error // Maps tenant ID to authentication error
}
type ErrorHandler struct {
@@ -97,15 +98,28 @@ func TryAuth(
return nil
}
- tenantIDs := make([]gid.TenantID, len(organizations))
- for i, org := range organizations {
- tenantIDs[i] = org.ID.TenantID()
+ // Validate organization access based on authentication requirements
+ // Only include organizations the user has proper authentication for
+ allowedTenantIDs := make([]gid.TenantID, 0, len(organizations))
+ authErrors := make(map[gid.TenantID]error)
+
+ for _, org := range organizations {
+ // Check if user has the required authentication for this organization
+ err := authSvc.CheckOrganizationAccess(ctx, user, org.ID, session)
+ if err == nil {
+ // User has proper authentication for this org
+ allowedTenantIDs = append(allowedTenantIDs, org.ID.TenantID())
+ } else {
+ // Store the authentication error for later use
+ authErrors[org.ID.TenantID()] = err
+ }
}
return &AuthResult{
Session: session,
User: user,
- TenantIDs: tenantIDs,
+ TenantIDs: allowedTenantIDs,
+ AuthErrors: authErrors,
}
}