Replug SAML configurations

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-24 11:15:31 +01:00
committed by Bryan Frimin
parent 600d763d18
commit b012b5ec42
24 changed files with 2230 additions and 121 deletions

View File

@@ -0,0 +1,123 @@
import {
graphql,
usePreloadedQuery,
useQueryLoader,
type PreloadedQuery,
} from "react-relay";
import type { SAMLSettingsPageQuery } from "./__generated__/SAMLSettingsPageQuery.graphql";
import { Suspense, use, useState } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
import { Breadcrumb, Button, Dialog, useDialogRef } from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { SAMLConfigurationList } from "./_components/SAMLConfigurationList";
import { NewSAMLConfigurationForm } from "./_components/NewSAMLConfigurationForm";
import {
EditSAMLConfigurationForm,
samlConfigurationFormQuery,
} from "./_components/EditSAMLConfigurationForm";
import type { EditSAMLConfigurationFormQuery } from "./_components/__generated__/EditSAMLConfigurationFormQuery.graphql";
import { SAMLDomainVerifyDialog } from "./_components/SAMLDomainVerifyDialog";
export const samlSettingsPageQuery = graphql`
query SAMLSettingsPageQuery($organizationId: ID!) {
organization: node(id: $organizationId) @required(action: THROW) {
...SAMLConfigurationListFragment
}
}
`;
export function SAMLSettingsPage(props: {
queryRef: PreloadedQuery<SAMLSettingsPageQuery>;
}) {
const { queryRef } = props;
const formDialogRef = useDialogRef();
const domainDialogRef = useDialogRef();
const [isEditing, setIsEditing] = useState<boolean>();
const [domainVerificationToken, setDomainVerificationToken] =
useState<string>();
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const { organization } = usePreloadedQuery(samlSettingsPageQuery, queryRef);
const [formQueryRef, loadFormQuery] =
useQueryLoader<EditSAMLConfigurationFormQuery>(samlConfigurationFormQuery);
const handleOpenFormDialog = (samlConfigurationId?: string) => {
setIsEditing(!!samlConfigurationId);
if (samlConfigurationId) {
loadFormQuery({ samlConfigurationId }, { fetchPolicy: "network-only" });
}
formDialogRef.current?.open();
};
const handleCloseFormDialog = () => {
setIsEditing(false);
formDialogRef.current?.close();
};
const handleOpenVerifyDomainDialog = (domainVerificationToken: string) => {
setDomainVerificationToken(domainVerificationToken);
domainDialogRef.current?.open();
};
const handleCloseVerifyDomainDialog = () => {
setDomainVerificationToken("");
formDialogRef.current?.close();
};
return (
<>
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-base font-medium">{__("SAML Single Sign-On")}</h2>
{isAuthorized("Organization", "createSAMLConfiguration") && (
<Button onClick={() => handleOpenFormDialog()}>
{__("Add Configuration")}
</Button>
)}
</div>
<SAMLConfigurationList
fKey={organization}
onNew={handleOpenFormDialog}
onEdit={(id: string) => handleOpenFormDialog(id)}
onVerifyDomain={handleOpenVerifyDomainDialog}
/>
</div>
<Dialog
ref={formDialogRef}
onClose={handleCloseFormDialog}
title={<Breadcrumb items={[__("SAML Settings"), __("Configure")]} />}
>
{isEditing ? (
<Suspense>
{formQueryRef && (
<EditSAMLConfigurationForm
queryRef={formQueryRef}
onUpdate={handleCloseFormDialog}
/>
)}
</Suspense>
) : (
<NewSAMLConfigurationForm onCreate={handleCloseFormDialog} />
)}
</Dialog>
<Dialog
ref={domainDialogRef}
onClose={handleCloseVerifyDomainDialog}
title={
<Breadcrumb items={[__("SAML Settings"), __("Verify Domain")]} />
}
>
{domainVerificationToken && (
<SAMLDomainVerifyDialog
key={domainVerificationToken}
domainVerificationToken={domainVerificationToken}
/>
)}
</Dialog>
</>
);
}

View File

@@ -0,0 +1,33 @@
import { useQueryLoader } from "react-relay";
import { SAMLSettingsPage, samlSettingsPageQuery } from "./SAMLSettingsPage";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { IAMRelayProvider } from "/providers/IAMRelayProvider";
import { useEffect } from "react";
import type { SAMLSettingsPageQuery } from "./__generated__/SAMLSettingsPageQuery.graphql";
function SAMLSettingsPageLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<SAMLSettingsPageQuery>(
samlSettingsPageQuery,
);
useEffect(() => {
loadQuery({
organizationId,
});
}, [loadQuery, organizationId]);
if (!queryRef) {
return null;
}
return <SAMLSettingsPage queryRef={queryRef} />;
}
export default function () {
return (
<IAMRelayProvider>
<SAMLSettingsPageLoader />
</IAMRelayProvider>
);
}

View File

@@ -0,0 +1,245 @@
/**
* @generated SignedSource<<57eb2489b4c62cb4b1df1a61bc135313>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type SAMLSettingsPageQuery$variables = {
organizationId: string;
};
export type SAMLSettingsPageQuery$data = {
readonly organization: {
readonly " $fragmentSpreads": FragmentRefs<"SAMLConfigurationListFragment">;
};
};
export type SAMLSettingsPageQuery = {
response: SAMLSettingsPageQuery$data;
variables: SAMLSettingsPageQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "organizationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v3 = [
{
"kind": "Literal",
"name": "first",
"value": 1000
}
],
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "SAMLSettingsPageQuery",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"args": null,
"kind": "FragmentSpread",
"name": "SAMLConfigurationListFragment"
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "SAMLSettingsPageQuery",
"selections": [
{
"alias": "organization",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
{
"alias": null,
"args": (v3/*: any*/),
"concreteType": "SAMLConfigurationConnection",
"kind": "LinkedField",
"name": "samlConfigurations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SAMLConfigurationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SAMLConfiguration",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
{
"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": "domainVerificationToken",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerifiedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "testLoginUrl",
"storageKey": null
},
(v2/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"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
}
],
"storageKey": "samlConfigurations(first:1000)"
},
{
"alias": null,
"args": (v3/*: any*/),
"filters": null,
"handle": "connection",
"key": "SAMLConfigurationListFragment_samlConfigurations",
"kind": "LinkedHandle",
"name": "samlConfigurations"
}
],
"type": "Organization",
"abstractKey": null
},
(v4/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "45740d8e7e6fba1ec2576fd62487e41e",
"id": null,
"metadata": {},
"name": "SAMLSettingsPageQuery",
"operationKind": "query",
"text": "query SAMLSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ...SAMLConfigurationListFragment\n id\n }\n}\n\nfragment SAMLConfigurationListFragment on Organization {\n samlConfigurations(first: 1000) {\n edges {\n node {\n id\n emailDomain\n enforcementPolicy\n domainVerificationToken\n domainVerifiedAt\n testLoginUrl\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n"
}
};
})();
(node as any).hash = "d2d6ef2a90f81a75df7ab0dd83ef52cf";
export default node;

View File

@@ -0,0 +1,111 @@
import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
import { graphql } from "relay-runtime";
import type { EditSAMLConfigurationForm_updateMutation } from "./__generated__/EditSAMLConfigurationForm_updateMutation.graphql";
import { useCallback } from "react";
import type { EditSAMLConfigurationFormQuery } from "./__generated__/EditSAMLConfigurationFormQuery.graphql";
import { useOrganizationId } from "/hooks/useOrganizationId";
import {
SAMLConfigurationForm,
type SAMLConfigurationFormData,
} from "./SAMLConfigurationForm";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
export const samlConfigurationFormQuery = graphql`
query EditSAMLConfigurationFormQuery($samlConfigurationId: ID!) {
samlConfiguration: node(id: $samlConfigurationId) @required(action: THROW) {
__typename
... on SAMLConfiguration {
id
emailDomain
enforcementPolicy
domainVerificationToken
domainVerifiedAt
testLoginUrl
idpEntityId
idpSsoUrl
idpCertificate
attributeMappings {
email
firstName
lastName
role
}
autoSignupEnabled
}
}
}
`;
const updateSAMLConfigurationMutation = graphql`
mutation EditSAMLConfigurationForm_updateMutation(
$input: UpdateSAMLConfigurationInput!
) {
updateSAMLConfiguration(input: $input) {
samlConfiguration {
id
emailDomain
enforcementPolicy
domainVerificationToken
domainVerifiedAt
testLoginUrl
}
}
}
`;
export function EditSAMLConfigurationForm(props: {
onUpdate: () => void;
queryRef: PreloadedQuery<EditSAMLConfigurationFormQuery>;
}) {
const { onUpdate, queryRef } = props;
const organizationId = useOrganizationId();
const { samlConfiguration } =
usePreloadedQuery<EditSAMLConfigurationFormQuery>(
samlConfigurationFormQuery,
queryRef,
);
if (samlConfiguration.__typename !== "SAMLConfiguration") {
throw new Error("node is not a SAML configuration");
}
const [update, isUpdating] =
useMutationWithToasts<EditSAMLConfigurationForm_updateMutation>(
updateSAMLConfigurationMutation,
{
successMessage: "SAML configuration updated successfully.",
errorMessage: "Failed to update SAML configuration. Please try again.",
},
);
const handleUpdate = useCallback(
(data: SAMLConfigurationFormData) => {
update({
variables: {
input: {
samlConfigurationId: samlConfiguration.id,
organizationId,
idpEntityId: data.idpEntityId,
idpSsoUrl: data.idpSsoUrl,
idpCertificate: data.idpCertificate,
autoSignupEnabled: data.autoSignupEnabled,
enforcementPolicy: data.enforcementPolicy,
attributeMappings: data.attributeMappings,
},
},
onCompleted: onUpdate,
});
},
[onUpdate, organizationId, samlConfiguration.id, update],
);
return (
<SAMLConfigurationForm
disabled={isUpdating}
initialValues={samlConfiguration}
isEditing
onSubmit={handleUpdate}
/>
);
}

View File

@@ -0,0 +1,73 @@
import { ConnectionHandler, graphql } from "react-relay";
import type { NewSAMLConfigurationForm_createMutation } from "./__generated__/NewSAMLConfigurationForm_createMutation.graphql";
import {
SAMLConfigurationForm,
type SAMLConfigurationFormData,
} from "./SAMLConfigurationForm";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { useCallback } from "react";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
const createSAMLConfigurationMutation = graphql`
mutation NewSAMLConfigurationForm_createMutation(
$input: CreateSAMLConfigurationInput!
$connections: [ID!]!
) {
createSAMLConfiguration(input: $input) {
samlConfigurationEdge @prependEdge(connections: $connections) {
node {
id
emailDomain
enforcementPolicy
domainVerificationToken
domainVerifiedAt
testLoginUrl
}
}
}
}
`;
export function NewSAMLConfigurationForm(props: { onCreate: () => void }) {
const { onCreate } = props;
const organizationId = useOrganizationId();
const [create, isCreating] =
useMutationWithToasts<NewSAMLConfigurationForm_createMutation>(
createSAMLConfigurationMutation,
{
successMessage: "SAML configuration created successfully.",
errorMessage: "Failed to create SAML configuration",
},
);
const handleCreate = useCallback(
(data: SAMLConfigurationFormData) => {
const connectionID = ConnectionHandler.getConnectionID(
organizationId,
"SAMLConfigurationListFragment_samlConfigurations",
);
create({
variables: {
input: {
organizationId,
emailDomain: data.emailDomain,
idpEntityId: data.idpEntityId,
idpSsoUrl: data.idpSsoUrl,
idpCertificate: data.idpCertificate,
autoSignupEnabled: data.autoSignupEnabled,
attributeMappings: data.attributeMappings,
},
connections: [connectionID],
},
onCompleted: onCreate,
});
},
[organizationId, create, onCreate],
);
return (
<SAMLConfigurationForm onSubmit={handleCreate} disabled={isCreating} />
);
}

View File

@@ -0,0 +1,255 @@
import { useTranslate } from "@probo/i18n";
import {
Button,
Checkbox,
DialogContent,
DialogFooter,
Field,
Label,
Option,
Select,
Textarea,
} from "@probo/ui";
import { Controller } from "react-hook-form";
import z from "zod";
import { useFormWithSchema } from "/hooks/useFormWithSchema";
const defaultValues: SAMLConfigurationFormData = {
emailDomain: "",
enforcementPolicy: "OPTIONAL" as const,
idpEntityId: "",
idpSsoUrl: "",
idpCertificate: "",
attributeMappings: {
email: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
firstName:
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
lastName: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
role: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role",
},
autoSignupEnabled: false,
};
const getEnforcementPolicyLabel = (
policy: string,
__: (key: string) => 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",
);
}
};
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"),
attributeMappings: z.object({
email: z.string().optional(),
firstName: z.string().optional(),
lastName: z.string().optional(),
role: z.string().optional(),
}),
autoSignupEnabled: z.boolean().default(false),
});
export type SAMLConfigurationFormData = z.infer<typeof samlConfigSchema>;
export function SAMLConfigurationForm(props: {
isEditing?: boolean;
disabled: boolean;
initialValues?: SAMLConfigurationFormData;
onSubmit: (data: SAMLConfigurationFormData) => void;
}) {
const {
disabled,
initialValues = defaultValues,
isEditing,
onSubmit,
} = props;
const { __ } = useTranslate();
const form = useFormWithSchema(samlConfigSchema, {
defaultValues: initialValues,
});
return (
<form
onSubmit={async (e) => {
e.preventDefault();
await form.handleSubmit(onSubmit)();
form.reset(form.getValues());
}}
>
<DialogContent padded className="space-y-6">
<div>
<h3 className="text-base font-medium mb-4">
{__("Basic Configuration")}
</h3>
<div className="space-y-4">
<div>
<Field
{...form.register("emailDomain")}
label={__("Email Domain") + " *"}
placeholder="example.com"
disabled={isEditing}
error={form.formState.errors.emailDomain?.message}
/>
<p className="text-xs text-gray-600 mt-1">
{isEditing
? __("Email domain cannot be changed after creation")
: __(
"The email domain this SAML configuration applies to (e.g., example.com)",
)}
</p>
</div>
{isEditing && (
<div>
<Label htmlFor="enforcementPolicy">
{__("Enforcement Policy") + " *"}
</Label>
<Controller
control={form.control}
name="enforcementPolicy"
render={({ field }) => (
<div className="mt-2">
<Select
value={field.value}
onValueChange={field.onChange}
>
<Option value="OPTIONAL">{__("Optional")}</Option>
<Option value="REQUIRED">{__("Required")}</Option>
<Option value="OFF">{__("Off")}</Option>
</Select>
</div>
)}
/>
{form.watch("enforcementPolicy") && (
<p className="text-xs text-gray-600 mt-2">
{getEnforcementPolicyLabel(
form.watch("enforcementPolicy"),
__,
)}
</p>
)}
</div>
)}
</div>
</div>
<div>
<h3 className="text-base font-medium mb-4">
{__("Identity Provider Configuration")}
</h3>
<div className="space-y-4">
<Field
{...form.register("idpEntityId")}
label={__("IdP Entity ID") + " *"}
placeholder="https://idp.example.com/metadata"
error={form.formState.errors.idpEntityId?.message}
/>
<Field
{...form.register("idpSsoUrl")}
label={__("IdP SSO URL") + " *"}
placeholder="https://idp.example.com/sso"
error={form.formState.errors.idpSsoUrl?.message}
/>
<div>
<Label htmlFor="idpCertificate">
{__("IdP X.509 Certificate") + " *"}
</Label>
<Textarea
{...form.register("idpCertificate")}
id="idpCertificate"
rows={6}
placeholder="-----BEGIN CERTIFICATE-----&#10;...&#10;-----END CERTIFICATE-----"
className="font-mono text-sm"
/>
{form.formState.errors.idpCertificate && (
<p className="text-sm text-red-600 mt-1">
{form.formState.errors.idpCertificate.message}
</p>
)}
</div>
</div>
</div>
<div>
<h3 className="text-base font-medium mb-4">
{__("Attribute Mapping")}
</h3>
<div className="space-y-4">
<Field
{...form.register("attributeMappings.email")}
label={__("Email Attribute")}
placeholder={defaultValues.attributeMappings.email}
error={form.formState.errors.attributeMappings?.email?.message}
/>
<Field
{...form.register("attributeMappings.firstName")}
label={__("First Name Attribute")}
placeholder={defaultValues.attributeMappings.firstName}
error={
form.formState.errors.attributeMappings?.firstName?.message
}
/>
<Field
{...form.register("attributeMappings.lastName")}
label={__("Last Name Attribute")}
placeholder={defaultValues.attributeMappings.lastName}
error={form.formState.errors.attributeMappings?.lastName?.message}
/>
<Field
{...form.register("attributeMappings.role")}
label={__("Role Attribute")}
placeholder={defaultValues.attributeMappings.role}
error={form.formState.errors.attributeMappings?.role?.message}
/>
</div>
</div>
<div>
<Controller
control={form.control}
name="autoSignupEnabled"
render={({ field }) => (
<div className="flex items-center gap-2">
<Checkbox
checked={field.value ?? false}
onChange={field.onChange}
/>
<Label htmlFor="autoSignupEnabled" className="cursor-pointer">
{__("Enable automatic user signup via SAML")}
</Label>
</div>
)}
/>
</div>
</DialogContent>
<DialogFooter>
<Button type="submit" disabled={disabled}>
{isEditing ? __("Update Configuration") : __("Create Configuration")}
</Button>
</DialogFooter>
</form>
);
}

View File

@@ -0,0 +1,241 @@
import { useTranslate } from "@probo/i18n";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
import {
Button,
Card,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
useConfirm,
} from "@probo/ui";
import { graphql } from "relay-runtime";
import { useFragment } from "react-relay";
import type {
SAMLConfigurationListFragment$data,
SAMLConfigurationListFragment$key,
} from "./__generated__/SAMLConfigurationListFragment.graphql";
import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import type { SAMLConfigurationList_deleteMutation } from "./__generated__/SAMLConfigurationList_deleteMutation.graphql";
import { useOrganizationId } from "/hooks/useOrganizationId";
import type { NodeOf } from "/types";
import { useCopy } from "@probo/hooks";
const fragment = graphql`
fragment SAMLConfigurationListFragment on Organization {
samlConfigurations(first: 1000)
@required(action: THROW)
@connection(key: "SAMLConfigurationListFragment_samlConfigurations") {
edges @required(action: THROW) {
node {
id
emailDomain
enforcementPolicy
domainVerificationToken
domainVerifiedAt
testLoginUrl
}
}
}
}
`;
const deleteMutation = graphql`
mutation SAMLConfigurationList_deleteMutation(
$input: DeleteSAMLConfigurationInput!
) {
deleteSAMLConfiguration(input: $input) {
deletedSamlConfigurationId
}
}
`;
export function SAMLConfigurationList(props: {
fKey: SAMLConfigurationListFragment$key;
onNew: () => void;
onEdit: (id: string) => void;
onVerifyDomain: (dnsVerificationToken: string) => void;
}) {
const { fKey, onNew, onEdit, onVerifyDomain } = props;
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const { isAuthorized } = use(PermissionsContext);
const confirm = useConfirm();
const [isCopied, copy] = useCopy();
const {
samlConfigurations: { edges: samlConfigurations },
} = useFragment<SAMLConfigurationListFragment$key>(fragment, fKey);
const [deleteSAMLConfiguration] =
useMutationWithToasts<SAMLConfigurationList_deleteMutation>(
deleteMutation,
{
successMessage: "SAML configuration deleted successfully.",
errorMessage: "Failed to delete SAML configuration. Please try again.",
},
);
const handleDelete = (
config: NodeOf<SAMLConfigurationListFragment$data["samlConfigurations"]>,
) => {
confirm(
async () => {
deleteSAMLConfiguration({
variables: {
input: {
organizationId,
samlConfigurationId: config.id,
},
},
});
},
{
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",
},
);
};
if (samlConfigurations.length === 0) {
return (
<Card padded>
<div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2">
{__("No SAML Configurations")}
</h3>
<p className="text-gray-600 mb-6">
{__(
"Set up SAML 2.0 single sign-on for your organization by adding a configuration for each email domain.",
)}
</p>
{isAuthorized("Organization", "createSAMLConfiguration") && (
<Button onClick={onNew}>
{__("Add Your First Configuration")}
</Button>
)}
</div>
</Card>
);
}
return (
<Table>
<Thead>
<Tr>
<Th>{__("Email Domain")}</Th>
<Th>{__("Domain Status")}</Th>
<Th>{__("SAML Status")}</Th>
<Th>{__("Enforcement")}</Th>
<Th>{__("SSO URL")}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{samlConfigurations.map(({ node: config }) => (
<Tr key={config.id}>
<Td>
<button
onClick={() => onEdit(config.id)}
className="font-semibold text-blue-600 hover:text-blue-800"
>
{config.emailDomain}
</button>
</Td>
<Td>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
config.domainVerifiedAt
? "bg-green-100 text-green-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{config.domainVerifiedAt
? __("Verified")
: __("Pending Verification")}
</span>
</Td>
<Td>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
config.enforcementPolicy !== "OFF"
? "bg-green-100 text-green-800"
: "bg-gray-100 text-gray-800"
}`}
>
{config.enforcementPolicy !== "OFF"
? __("Enabled")
: __("Disabled")}
</span>
</Td>
<Td>{config.enforcementPolicy}</Td>
<Td>
{config.domainVerifiedAt && config.enforcementPolicy !== "OFF" ? (
<button
onClick={() => copy(config.testLoginUrl)}
className="text-blue-600 hover:text-blue-800"
>
{isCopied ? __("Copied!") : __("Copy URL")}
</button>
) : (
<span className="text-gray-400">—</span>
)}
</Td>
<Td width={180} className="text-end">
<div className="flex gap-2 justify-end">
{config.domainVerifiedAt ? (
<>
{isAuthorized(
"SAMLConfiguration",
"updateSAMLConfiguration",
) && (
<Button
variant="secondary"
onClick={() => onEdit(config.id)}
>
{__("Edit")}
</Button>
)}
</>
) : (
<>
{isAuthorized("Organization", "verifyDomain") &&
!!config.domainVerificationToken && (
<Button
variant="primary"
onClick={() =>
onVerifyDomain(config.domainVerificationToken!)
}
>
{__("Verify Domain")}
</Button>
)}
{isAuthorized("Organization", "deleteOrganization") && (
<Button
variant="danger"
onClick={() => handleDelete(config)}
>
{__("Delete")}
</Button>
)}
</>
)}
</div>
</Td>
</Tr>
))}
</Tbody>
</Table>
);
}

View File

@@ -0,0 +1,75 @@
import { useCopy } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { DialogContent, Button } from "@probo/ui";
export function SAMLDomainVerifyDialog(props: {
domainVerificationToken: string;
}) {
const { domainVerificationToken } = props;
const { __ } = useTranslate();
const dnsRecord = `probo-verification=${domainVerificationToken}`;
const [isCopied, copy] = useCopy();
return (
<>
<DialogContent padded className="space-y-6">
<div>
<h3 className="text-base font-medium mb-4">
{__("Verify Domain Ownership")}
</h3>
<p className="text-sm text-gray-600 mb-4">
{__(
"Add the following TXT record to your domain's DNS configuration to verify ownership:",
)}
</p>
<div className="bg-gray-50 rounded-lg p-4 mb-4">
<div className="space-y-2">
<div>
<span className="font-semibold text-sm">
{__("Host/Name:")}
</span>
<code className="ml-2 bg-white px-2 py-1 rounded text-sm">
@
</code>
<span className="ml-2 text-xs text-gray-600">
{__("or use your domain name")}
</span>
</div>
<div>
<span className="font-semibold text-sm">{__("Type:")}</span>
<code className="ml-2 bg-white px-2 py-1 rounded text-sm">
TXT
</code>
</div>
<div>
<span className="font-semibold text-sm">{__("Value:")}</span>
<div className="mt-1 flex items-center gap-2">
<code className="flex-1 bg-white px-2 py-1 rounded text-sm break-all font-mono">
{dnsRecord}
</code>
<Button
type="button"
variant="secondary"
onClick={() => copy(dnsRecord)}
>
{isCopied ? __("Copied!") : __("Copy")}
</Button>
</div>
</div>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800">
<strong>{__("Note:")}</strong>{" "}
{__(
"DNS changes may take up to 48 hours to propagate, but typically complete within a few minutes.",
)}
</p>
</div>
</div>
</DialogContent>
</>
);
}

View File

@@ -0,0 +1,273 @@
/**
* @generated SignedSource<<3da2840acb86d86a80715539be18fba0>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED";
export type EditSAMLConfigurationFormQuery$variables = {
samlConfigurationId: string;
};
export type EditSAMLConfigurationFormQuery$data = {
readonly samlConfiguration: {
readonly __typename: "SAMLConfiguration";
readonly attributeMappings: {
readonly email: string;
readonly firstName: string;
readonly lastName: string;
readonly role: string;
};
readonly autoSignupEnabled: boolean;
readonly domainVerificationToken: string | null | undefined;
readonly domainVerifiedAt: any | null | undefined;
readonly emailDomain: string;
readonly enforcementPolicy: SAMLEnforcementPolicy;
readonly id: string;
readonly idpCertificate: string;
readonly idpEntityId: string;
readonly idpSsoUrl: string;
readonly testLoginUrl: string;
} | {
// This will never be '%other', but we need some
// value in case none of the concrete values match.
readonly __typename: "%other";
};
};
export type EditSAMLConfigurationFormQuery = {
response: EditSAMLConfigurationFormQuery$data;
variables: EditSAMLConfigurationFormQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "samlConfigurationId"
}
],
v1 = [
{
"kind": "Variable",
"name": "id",
"variableName": "samlConfigurationId"
}
],
v2 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "emailDomain",
"storageKey": null
},
v5 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "enforcementPolicy",
"storageKey": null
},
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerificationToken",
"storageKey": null
},
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerifiedAt",
"storageKey": null
},
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "testLoginUrl",
"storageKey": null
},
v9 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "idpEntityId",
"storageKey": null
},
v10 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "idpSsoUrl",
"storageKey": null
},
v11 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "idpCertificate",
"storageKey": null
},
v12 = {
"alias": null,
"args": null,
"concreteType": "SAMLAttributeMappings",
"kind": "LinkedField",
"name": "attributeMappings",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "email",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "firstName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "lastName",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "role",
"storageKey": null
}
],
"storageKey": null
},
v13 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "autoSignupEnabled",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "EditSAMLConfigurationFormQuery",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": "samlConfiguration",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v3/*: any*/),
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
(v13/*: any*/)
],
"type": "SAMLConfiguration",
"abstractKey": null
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EditSAMLConfigurationFormQuery",
"selections": [
{
"alias": "samlConfiguration",
"args": (v1/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v4/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v7/*: any*/),
(v8/*: any*/),
(v9/*: any*/),
(v10/*: any*/),
(v11/*: any*/),
(v12/*: any*/),
(v13/*: any*/)
],
"type": "SAMLConfiguration",
"abstractKey": null
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "47c1ce92e8ba0395fa803b343d093879",
"id": null,
"metadata": {},
"name": "EditSAMLConfigurationFormQuery",
"operationKind": "query",
"text": "query EditSAMLConfigurationFormQuery(\n $samlConfigurationId: ID!\n) {\n samlConfiguration: node(id: $samlConfigurationId) {\n __typename\n ... on SAMLConfiguration {\n id\n emailDomain\n enforcementPolicy\n domainVerificationToken\n domainVerifiedAt\n testLoginUrl\n idpEntityId\n idpSsoUrl\n idpCertificate\n attributeMappings {\n email\n firstName\n lastName\n role\n }\n autoSignupEnabled\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "d31326e3e4423e62d8acde5735d9b578";
export default node;

View File

@@ -0,0 +1,159 @@
/**
* @generated SignedSource<<33cdb4d30c06225b656ba2b3f27830eb>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED";
export type UpdateSAMLConfigurationInput = {
attributeMappings?: SAMLAttributeMappingsInput | null | undefined;
autoSignupEnabled?: boolean | null | undefined;
enforcementPolicy: SAMLEnforcementPolicy;
idpCertificate?: string | null | undefined;
idpEntityId?: string | null | undefined;
idpSsoUrl?: string | null | undefined;
organizationId: string;
samlConfigurationId: string;
};
export type SAMLAttributeMappingsInput = {
email?: string | null | undefined;
firstName?: string | null | undefined;
lastName?: string | null | undefined;
role?: string | null | undefined;
};
export type EditSAMLConfigurationForm_updateMutation$variables = {
input: UpdateSAMLConfigurationInput;
};
export type EditSAMLConfigurationForm_updateMutation$data = {
readonly updateSAMLConfiguration: {
readonly samlConfiguration: {
readonly domainVerificationToken: string | null | undefined;
readonly domainVerifiedAt: any | null | undefined;
readonly emailDomain: string;
readonly enforcementPolicy: SAMLEnforcementPolicy;
readonly id: string;
readonly testLoginUrl: string;
} | null | undefined;
} | null | undefined;
};
export type EditSAMLConfigurationForm_updateMutation = {
response: EditSAMLConfigurationForm_updateMutation$data;
variables: EditSAMLConfigurationForm_updateMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "UpdateSAMLConfigurationPayload",
"kind": "LinkedField",
"name": "updateSAMLConfiguration",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SAMLConfiguration",
"kind": "LinkedField",
"name": "samlConfiguration",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"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": "domainVerificationToken",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerifiedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "testLoginUrl",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "EditSAMLConfigurationForm_updateMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "EditSAMLConfigurationForm_updateMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "56d29623294c243a01bfbc2ab4310d28",
"id": null,
"metadata": {},
"name": "EditSAMLConfigurationForm_updateMutation",
"operationKind": "mutation",
"text": "mutation EditSAMLConfigurationForm_updateMutation(\n $input: UpdateSAMLConfigurationInput!\n) {\n updateSAMLConfiguration(input: $input) {\n samlConfiguration {\n id\n emailDomain\n enforcementPolicy\n domainVerificationToken\n domainVerifiedAt\n testLoginUrl\n }\n }\n}\n"
}
};
})();
(node as any).hash = "b98e701514c179bba0f0ce63aae0c0b8";
export default node;

View File

@@ -0,0 +1,211 @@
/**
* @generated SignedSource<<0d0fdbf079f1a72545fa363ce60cd2f7>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type SAMLEnforcementPolicy = "OFF" | "OPTIONAL" | "REQUIRED";
export type CreateSAMLConfigurationInput = {
attributeMappings?: SAMLAttributeMappingsInput | null | undefined;
autoSignupEnabled: boolean;
emailDomain: string;
idpCertificate: string;
idpEntityId: string;
idpSsoUrl: string;
organizationId: string;
};
export type SAMLAttributeMappingsInput = {
email?: string | null | undefined;
firstName?: string | null | undefined;
lastName?: string | null | undefined;
role?: string | null | undefined;
};
export type NewSAMLConfigurationForm_createMutation$variables = {
connections: ReadonlyArray<string>;
input: CreateSAMLConfigurationInput;
};
export type NewSAMLConfigurationForm_createMutation$data = {
readonly createSAMLConfiguration: {
readonly samlConfigurationEdge: {
readonly node: {
readonly domainVerificationToken: string | null | undefined;
readonly domainVerifiedAt: any | null | undefined;
readonly emailDomain: string;
readonly enforcementPolicy: SAMLEnforcementPolicy;
readonly id: string;
readonly testLoginUrl: string;
};
};
} | null | undefined;
};
export type NewSAMLConfigurationForm_createMutation = {
response: NewSAMLConfigurationForm_createMutation$data;
variables: NewSAMLConfigurationForm_createMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "connections"
},
v1 = {
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
},
v2 = [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
v3 = {
"alias": null,
"args": null,
"concreteType": "SAMLConfigurationEdge",
"kind": "LinkedField",
"name": "samlConfigurationEdge",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SAMLConfiguration",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"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": "domainVerificationToken",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerifiedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "testLoginUrl",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": [
(v0/*: any*/),
(v1/*: any*/)
],
"kind": "Fragment",
"metadata": null,
"name": "NewSAMLConfigurationForm_createMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateSAMLConfigurationPayload",
"kind": "LinkedField",
"name": "createSAMLConfiguration",
"plural": false,
"selections": [
(v3/*: any*/)
],
"storageKey": null
}
],
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": [
(v1/*: any*/),
(v0/*: any*/)
],
"kind": "Operation",
"name": "NewSAMLConfigurationForm_createMutation",
"selections": [
{
"alias": null,
"args": (v2/*: any*/),
"concreteType": "CreateSAMLConfigurationPayload",
"kind": "LinkedField",
"name": "createSAMLConfiguration",
"plural": false,
"selections": [
(v3/*: any*/),
{
"alias": null,
"args": null,
"filters": null,
"handle": "prependEdge",
"key": "",
"kind": "LinkedHandle",
"name": "samlConfigurationEdge",
"handleArgs": [
{
"kind": "Variable",
"name": "connections",
"variableName": "connections"
}
]
}
],
"storageKey": null
}
]
},
"params": {
"cacheID": "969d5fb34995f5cce359c792cbe1ff49",
"id": null,
"metadata": {},
"name": "NewSAMLConfigurationForm_createMutation",
"operationKind": "mutation",
"text": "mutation NewSAMLConfigurationForm_createMutation(\n $input: CreateSAMLConfigurationInput!\n) {\n createSAMLConfiguration(input: $input) {\n samlConfigurationEdge {\n node {\n id\n emailDomain\n enforcementPolicy\n domainVerificationToken\n domainVerifiedAt\n testLoginUrl\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "5a3b2e5d219b40ca096eece75d7b63b3";
export default node;

View File

@@ -0,0 +1,180 @@
/**
* @generated SignedSource<<12fade1fd1f7f3ae53595970b6c2c47e>>
* @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 SAMLConfigurationListFragment$data = {
readonly samlConfigurations: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly domainVerificationToken: string | null | undefined;
readonly domainVerifiedAt: any | null | undefined;
readonly emailDomain: string;
readonly enforcementPolicy: SAMLEnforcementPolicy;
readonly id: string;
readonly testLoginUrl: string;
};
}>;
};
readonly " $fragmentType": "SAMLConfigurationListFragment";
};
export type SAMLConfigurationListFragment$key = {
readonly " $data"?: SAMLConfigurationListFragment$data;
readonly " $fragmentSpreads": FragmentRefs<"SAMLConfigurationListFragment">;
};
const node: ReaderFragment = {
"argumentDefinitions": [],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": null,
"cursor": null,
"direction": "forward",
"path": [
"samlConfigurations"
]
}
]
},
"name": "SAMLConfigurationListFragment",
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": "samlConfigurations",
"args": null,
"concreteType": "SAMLConfigurationConnection",
"kind": "LinkedField",
"name": "__SAMLConfigurationListFragment_samlConfigurations_connection",
"plural": false,
"selections": [
{
"kind": "RequiredField",
"field": {
"alias": null,
"args": null,
"concreteType": "SAMLConfigurationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "SAMLConfiguration",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"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": "domainVerificationToken",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "domainVerifiedAt",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "testLoginUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
"action": "THROW"
},
{
"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
}
],
"storageKey": null
},
"action": "THROW"
}
],
"type": "Organization",
"abstractKey": null
};
(node as any).hash = "79203ccae6f8e3a1967b463f0f3f3c76";
export default node;

View File

@@ -0,0 +1,93 @@
/**
* @generated SignedSource<<4ce15b49974b7e512f334f47765caaa4>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
export type DeleteSAMLConfigurationInput = {
organizationId: string;
samlConfigurationId: string;
};
export type SAMLConfigurationList_deleteMutation$variables = {
input: DeleteSAMLConfigurationInput;
};
export type SAMLConfigurationList_deleteMutation$data = {
readonly deleteSAMLConfiguration: {
readonly deletedSamlConfigurationId: string;
} | null | undefined;
};
export type SAMLConfigurationList_deleteMutation = {
response: SAMLConfigurationList_deleteMutation$data;
variables: SAMLConfigurationList_deleteMutation$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "input"
}
],
v1 = [
{
"alias": null,
"args": [
{
"kind": "Variable",
"name": "input",
"variableName": "input"
}
],
"concreteType": "DeleteSAMLConfigurationPayload",
"kind": "LinkedField",
"name": "deleteSAMLConfiguration",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "deletedSamlConfigurationId",
"storageKey": null
}
],
"storageKey": null
}
];
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "SAMLConfigurationList_deleteMutation",
"selections": (v1/*: any*/),
"type": "Mutation",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "SAMLConfigurationList_deleteMutation",
"selections": (v1/*: any*/)
},
"params": {
"cacheID": "cb46bb31e5d10f0ddad38795a72b1ab1",
"id": null,
"metadata": {},
"name": "SAMLConfigurationList_deleteMutation",
"operationKind": "mutation",
"text": "mutation SAMLConfigurationList_deleteMutation(\n $input: DeleteSAMLConfigurationInput!\n) {\n deleteSAMLConfiguration(input: $input) {\n deletedSamlConfigurationId\n }\n}\n"
}
};
})();
(node as any).hash = "2ebbe76aae843f00570f7ae1848ee995";
export default node;

View File

@@ -220,12 +220,13 @@ const routes = [
() => import("./pages/organizations/settings/DomainSettingsTab"),
),
},
// {
// path: "saml-sso",
// Component: lazy(
// () => import("./pages/organizations/settings/SAMLSettingsTab")
// ),
// },
{
path: "saml-sso",
Component: lazy(
() =>
import("./pages/iam/organizations/settings/SAMLSettingsPageLoader"),
),
},
],
},
...riskRoutes,

View File

@@ -155,7 +155,7 @@
"value": "admin@example.com",
"temporary": false
}
],
]
},
{
"username": "viewer@example.com",

View File

@@ -4,3 +4,4 @@ export { useRefSync } from "./useRefSync";
export { useList } from "./useList";
export { useStateWithRef } from "./useStateWithRef";
export { useCleanup } from "./useCleanup";
export { useCopy } from "./useCopy";

View File

@@ -0,0 +1,18 @@
import { useRef, useState } from "react";
export function useCopy(): [boolean, (value: string) => void] {
const [copiedValue, setCopiedValue] = useState<string>();
const lastCopiedValueRef = useRef<string>("");
const handleCopy = (value: string) => {
lastCopiedValueRef.current = value;
navigator.clipboard.writeText(value);
setCopiedValue(value);
setTimeout(() => {
setCopiedValue(undefined);
lastCopiedValueRef.current = "";
}, 2000);
};
return [copiedValue === lastCopiedValueRef.current, handleCopy];
}

View File

@@ -133,11 +133,8 @@ var IAMAdminPolicy = policy.NewPolicy(
ActionIAMInvitationGet,
ActionIAMInvitationDelete,
).WithSID("invitation-admin-access"),
// Can view SAML configurations
policy.Allow(
ActionIAMSAMLConfigurationGet,
ActionIAMSAMLConfigurationList,
).WithSID("saml-viewer-access"),
// Can view and update SAML configurations
policy.Allow(ActionIAMSAMLConfigurationGet).WithSID("saml-configuration-admin-access"),
// Cannot delete organization
policy.Deny(ActionIAMOrganizationDelete).WithSID("deny-org-delete"),
// Cannot remove members (only owner can)

View File

@@ -99,6 +99,14 @@ func (s *Service) Run(ctx context.Context) error {
}
}
func (s *Service) GetEntityID() string {
return fmt.Sprintf("%s/connect/saml/metadata", s.baseURL)
}
func (s *Service) GetAcsURL() string {
return fmt.Sprintf("%s/connect/saml/consume", s.baseURL)
}
func (s *Service) GenerateSpMetadata() ([]byte, error) {
sp := s.baseServiceProvider()
return xml.MarshalIndent(sp.Metadata(), "", " ")

View File

@@ -219,3 +219,31 @@ func (s *Service) GetSession(ctx context.Context, sessionID gid.GID) (*coredata.
return session, nil
}
func (s *Service) GetSAMLconfiguration(ctx context.Context, samlConfigurationID gid.GID) (*coredata.SAMLConfiguration, error) {
var (
scope = coredata.NewScopeFromObjectID(samlConfigurationID)
samlConfiguration = &coredata.SAMLConfiguration{}
)
err := s.pg.WithConn(
ctx,
func(conn pg.Conn) error {
err := samlConfiguration.LoadByID(ctx, conn, scope, samlConfigurationID)
if err != nil {
if err == coredata.ErrResourceNotFound {
return saml.NewSAMLConfigurationNotFoundError(samlConfigurationID)
}
return fmt.Errorf("cannot load SAML configuration: %w", err)
}
return nil
},
)
if err != nil {
return nil, err
}
return samlConfiguration, nil
}

View File

@@ -300,8 +300,7 @@ type SAMLConfiguration implements Node {
autoSignupEnabled: Boolean!
createdAt: Datetime!
updatedAt: Datetime!
spMetadataUrl: String!
testLoginUrl: String!
testLoginUrl: String! @goField(forceResolver: true)
attributeMappings: SAMLAttributeMappings!
}
@@ -640,7 +639,7 @@ input UpdateSAMLConfigurationInput {
idpSsoUrl: String
idpCertificate: String
autoSignupEnabled: Boolean
enforcementPolicy: SAMLEnforcementPolicy
enforcementPolicy: SAMLEnforcementPolicy!
attributeMappings: SAMLAttributeMappingsInput
}

View File

@@ -55,6 +55,7 @@ type ResolverRoot interface {
Organization() OrganizationResolver
PersonalAPIKeyConnection() PersonalAPIKeyConnectionResolver
Query() QueryResolver
SAMLConfiguration() SAMLConfigurationResolver
SAMLConfigurationConnection() SAMLConfigurationConnectionResolver
Session() SessionResolver
SessionConnection() SessionConnectionResolver
@@ -339,7 +340,6 @@ type ComplexityRoot struct {
IdpCertificate func(childComplexity int) int
IdpEntityID func(childComplexity int) int
IdpSsoURL func(childComplexity int) int
SpMetadataURL func(childComplexity int) int
TestLoginURL func(childComplexity int) int
UpdatedAt func(childComplexity int) int
}
@@ -485,6 +485,9 @@ type QueryResolver interface {
Viewer(ctx context.Context) (*types.Identity, error)
CheckSSOAvailability(ctx context.Context, email string) (*types.SSOAvailability, error)
}
type SAMLConfigurationResolver interface {
TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error)
}
type SAMLConfigurationConnectionResolver interface {
TotalCount(ctx context.Context, obj *types.SAMLConfigurationConnection) (*int, error)
}
@@ -1636,12 +1639,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.SAMLConfiguration.IdpSsoURL(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
@@ -2278,8 +2275,7 @@ type SAMLConfiguration implements Node {
autoSignupEnabled: Boolean!
createdAt: Datetime!
updatedAt: Datetime!
spMetadataUrl: String!
testLoginUrl: String!
testLoginUrl: String! @goField(forceResolver: true)
attributeMappings: SAMLAttributeMappings!
}
@@ -2618,7 +2614,7 @@ input UpdateSAMLConfigurationInput {
idpSsoUrl: String
idpCertificate: String
autoSignupEnabled: Boolean
enforcementPolicy: SAMLEnforcementPolicy
enforcementPolicy: SAMLEnforcementPolicy!
attributeMappings: SAMLAttributeMappingsInput
}
@@ -9532,35 +9528,6 @@ func (ec *executionContext) fieldContext_SAMLConfiguration_updatedAt(_ context.C
return fc, nil
}
func (ec *executionContext) _SAMLConfiguration_spMetadataUrl(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
ec.OperationContext,
field,
ec.fieldContext_SAMLConfiguration_spMetadataUrl,
func(ctx context.Context) (any, error) {
return obj.SpMetadataURL, nil
},
nil,
ec.marshalNString2string,
true,
true,
)
}
func (ec *executionContext) fieldContext_SAMLConfiguration_spMetadataUrl(_ 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_testLoginUrl(ctx context.Context, field graphql.CollectedField, obj *types.SAMLConfiguration) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -9568,7 +9535,7 @@ func (ec *executionContext) _SAMLConfiguration_testLoginUrl(ctx context.Context,
field,
ec.fieldContext_SAMLConfiguration_testLoginUrl,
func(ctx context.Context) (any, error) {
return obj.TestLoginURL, nil
return ec.resolvers.SAMLConfiguration().TestLoginURL(ctx, obj)
},
nil,
ec.marshalNString2string,
@@ -9581,8 +9548,8 @@ func (ec *executionContext) fieldContext_SAMLConfiguration_testLoginUrl(_ contex
fc = &graphql.FieldContext{
Object: "SAMLConfiguration",
Field: field,
IsMethod: false,
IsResolver: false,
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")
},
@@ -9778,8 +9745,6 @@ func (ec *executionContext) fieldContext_SAMLConfigurationEdge_node(_ context.Co
return ec.fieldContext_SAMLConfiguration_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_SAMLConfiguration_updatedAt(ctx, field)
case "spMetadataUrl":
return ec.fieldContext_SAMLConfiguration_spMetadataUrl(ctx, field)
case "testLoginUrl":
return ec.fieldContext_SAMLConfiguration_testLoginUrl(ctx, field)
case "attributeMappings":
@@ -10707,8 +10672,6 @@ func (ec *executionContext) fieldContext_UpdateSAMLConfigurationPayload_samlConf
return ec.fieldContext_SAMLConfiguration_createdAt(ctx, field)
case "updatedAt":
return ec.fieldContext_SAMLConfiguration_updatedAt(ctx, field)
case "spMetadataUrl":
return ec.fieldContext_SAMLConfiguration_spMetadataUrl(ctx, field)
case "testLoginUrl":
return ec.fieldContext_SAMLConfiguration_testLoginUrl(ctx, field)
case "attributeMappings":
@@ -13214,7 +13177,7 @@ func (ec *executionContext) unmarshalInputUpdateSAMLConfigurationInput(ctx conte
it.AutoSignupEnabled = data
case "enforcementPolicy":
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("enforcementPolicy"))
data, err := ec.unmarshalOSAMLEnforcementPolicy2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy(ctx, v)
data, err := ec.unmarshalNSAMLEnforcementPolicy2goᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy(ctx, v)
if err != nil {
return it, err
}
@@ -16030,17 +15993,17 @@ func (ec *executionContext) _SAMLConfiguration(ctx context.Context, sel ast.Sele
case "id":
out.Values[i] = ec._SAMLConfiguration_id(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "emailDomain":
out.Values[i] = ec._SAMLConfiguration_emailDomain(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "enforcementPolicy":
out.Values[i] = ec._SAMLConfiguration_enforcementPolicy(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "domainVerifiedAt":
out.Values[i] = ec._SAMLConfiguration_domainVerifiedAt(ctx, field, obj)
@@ -16049,47 +16012,73 @@ func (ec *executionContext) _SAMLConfiguration(ctx context.Context, sel ast.Sele
case "idpEntityId":
out.Values[i] = ec._SAMLConfiguration_idpEntityId(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "idpSsoUrl":
out.Values[i] = ec._SAMLConfiguration_idpSsoUrl(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "idpCertificate":
out.Values[i] = ec._SAMLConfiguration_idpCertificate(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "autoSignupEnabled":
out.Values[i] = ec._SAMLConfiguration_autoSignupEnabled(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "createdAt":
out.Values[i] = ec._SAMLConfiguration_createdAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "updatedAt":
out.Values[i] = ec._SAMLConfiguration_updatedAt(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
}
case "spMetadataUrl":
out.Values[i] = ec._SAMLConfiguration_spMetadataUrl(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
case "testLoginUrl":
out.Values[i] = ec._SAMLConfiguration_testLoginUrl(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
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 "attributeMappings":
out.Values[i] = ec._SAMLConfiguration_attributeMappings(ctx, field, obj)
if out.Values[i] == graphql.Null {
out.Invalids++
atomic.AddUint32(&out.Invalids, 1)
}
default:
panic("unknown field " + strconv.Quote(field.Name))
@@ -18881,38 +18870,6 @@ func (ec *executionContext) marshalOSAMLConfigurationConnection2ᚖgoᚗproboᚗ
return ec._SAMLConfigurationConnection(ctx, sel, v)
}
func (ec *executionContext) unmarshalOSAMLEnforcementPolicy2ᚖgoᚗproboᚗincᚋ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ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy[tmp]
return &res, graphql.ErrorOnPath(ctx, err)
}
func (ec *executionContext) marshalOSAMLEnforcementPolicy2ᚖgoᚗproboᚗincᚋ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ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy[*v])
return res
}
var (
unmarshalOSAMLEnforcementPolicy2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy = map[string]coredata.SAMLEnforcementPolicy{
"OFF": coredata.SAMLEnforcementPolicyOff,
"OPTIONAL": coredata.SAMLEnforcementPolicyOptional,
"REQUIRED": coredata.SAMLEnforcementPolicyRequired,
}
marshalOSAMLEnforcementPolicy2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋcoredataᚐSAMLEnforcementPolicy = map[coredata.SAMLEnforcementPolicy]string{
coredata.SAMLEnforcementPolicyOff: "OFF",
coredata.SAMLEnforcementPolicyOptional: "OPTIONAL",
coredata.SAMLEnforcementPolicyRequired: "REQUIRED",
}
)
func (ec *executionContext) marshalOSession2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSession(ctx context.Context, sel ast.SelectionSet, v *types.Session) graphql.Marshaler {
if v == nil {
return graphql.Null

View File

@@ -371,7 +371,6 @@ type SAMLConfiguration struct {
AutoSignupEnabled bool `json:"autoSignupEnabled"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
SpMetadataURL string `json:"spMetadataUrl"`
TestLoginURL string `json:"testLoginUrl"`
AttributeMappings *SAMLAttributeMappings `json:"attributeMappings"`
}
@@ -472,14 +471,14 @@ type UpdatePersonalAPIKeyPayload struct {
}
type UpdateSAMLConfigurationInput struct {
OrganizationID gid.GID `json:"organizationId"`
SamlConfigurationID gid.GID `json:"samlConfigurationId"`
IdpEntityID *string `json:"idpEntityId,omitempty"`
IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
IdpCertificate *string `json:"idpCertificate,omitempty"`
AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
EnforcementPolicy *coredata.SAMLEnforcementPolicy `json:"enforcementPolicy,omitempty"`
AttributeMappings *SAMLAttributeMappingsInput `json:"attributeMappings,omitempty"`
OrganizationID gid.GID `json:"organizationId"`
SamlConfigurationID gid.GID `json:"samlConfigurationId"`
IdpEntityID *string `json:"idpEntityId,omitempty"`
IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
IdpCertificate *string `json:"idpCertificate,omitempty"`
AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
AttributeMappings *SAMLAttributeMappingsInput `json:"attributeMappings,omitempty"`
}
type UpdateSAMLConfigurationPayload struct {

View File

@@ -9,6 +9,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/99designs/gqlgen/graphql"
@@ -933,6 +934,7 @@ func (r *mutationResolver) UpdateSAMLConfiguration(ctx context.Context, input ty
IdPSsoURL: input.IdpSsoURL,
IdPCertificate: input.IdpCertificate,
AutoSignupEnabled: input.AutoSignupEnabled,
EnforcementPolicy: &input.EnforcementPolicy,
}
if input.AttributeMappings != nil {
@@ -1163,6 +1165,16 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
return types.NewInvitation(invitation), nil
}
case coredata.SAMLConfigurationEntityType:
action = iam.ActionIAMSAMLConfigurationGet
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
samlConfiguration, err := r.iam.GetSAMLconfiguration(ctx, id)
if err != nil {
return nil, err
}
return types.NewSAMLConfiguration(samlConfiguration), nil
}
default:
return nil, fmt.Errorf("unsupported entity type: %d", id.EntityType())
}
@@ -1231,6 +1243,17 @@ func (r *queryResolver) CheckSSOAvailability(ctx context.Context, email string)
panic(fmt.Errorf("not implemented: CheckSSOAvailability - checkSSOAvailability"))
}
// TestLoginURL is the resolver for the testLoginUrl field.
func (r *sAMLConfigurationResolver) TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error) {
entityID := r.iam.SAMLService.GetEntityID()
parts := strings.Split(entityID, "/connect/saml/metadata")
if len(parts) != 2 {
return "", fmt.Errorf("invalid entity ID format")
}
return fmt.Sprintf("%s/connect/saml/login/%s", parts[0], obj.ID), nil
}
// TotalCount is the resolver for the totalCount field.
func (r *sAMLConfigurationConnectionResolver) TotalCount(ctx context.Context, obj *types.SAMLConfigurationConnection) (*int, error) {
switch obj.Resolver.(type) {
@@ -1314,6 +1337,11 @@ func (r *Resolver) PersonalAPIKeyConnection() schema.PersonalAPIKeyConnectionRes
// Query returns schema.QueryResolver implementation.
func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} }
// SAMLConfiguration returns schema.SAMLConfigurationResolver implementation.
func (r *Resolver) SAMLConfiguration() schema.SAMLConfigurationResolver {
return &sAMLConfigurationResolver{r}
}
// SAMLConfigurationConnection returns schema.SAMLConfigurationConnectionResolver implementation.
func (r *Resolver) SAMLConfigurationConnection() schema.SAMLConfigurationConnectionResolver {
return &sAMLConfigurationConnectionResolver{r}
@@ -1336,6 +1364,7 @@ type mutationResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver }
type personalAPIKeyConnectionResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }
type sAMLConfigurationResolver struct{ *Resolver }
type sAMLConfigurationConnectionResolver struct{ *Resolver }
type sessionResolver struct{ *Resolver }
type sessionConnectionResolver struct{ *Resolver }