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"), () => import("./pages/organizations/settings/DomainSettingsTab"),
), ),
}, },
// { {
// path: "saml-sso", path: "saml-sso",
// Component: lazy( Component: lazy(
// () => import("./pages/organizations/settings/SAMLSettingsTab") () =>
// ), import("./pages/iam/organizations/settings/SAMLSettingsPageLoader"),
// }, ),
},
], ],
}, },
...riskRoutes, ...riskRoutes,

View File

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

View File

@@ -4,3 +4,4 @@ export { useRefSync } from "./useRefSync";
export { useList } from "./useList"; export { useList } from "./useList";
export { useStateWithRef } from "./useStateWithRef"; export { useStateWithRef } from "./useStateWithRef";
export { useCleanup } from "./useCleanup"; 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, ActionIAMInvitationGet,
ActionIAMInvitationDelete, ActionIAMInvitationDelete,
).WithSID("invitation-admin-access"), ).WithSID("invitation-admin-access"),
// Can view SAML configurations // Can view and update SAML configurations
policy.Allow( policy.Allow(ActionIAMSAMLConfigurationGet).WithSID("saml-configuration-admin-access"),
ActionIAMSAMLConfigurationGet,
ActionIAMSAMLConfigurationList,
).WithSID("saml-viewer-access"),
// Cannot delete organization // Cannot delete organization
policy.Deny(ActionIAMOrganizationDelete).WithSID("deny-org-delete"), policy.Deny(ActionIAMOrganizationDelete).WithSID("deny-org-delete"),
// Cannot remove members (only owner can) // 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) { func (s *Service) GenerateSpMetadata() ([]byte, error) {
sp := s.baseServiceProvider() sp := s.baseServiceProvider()
return xml.MarshalIndent(sp.Metadata(), "", " ") 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 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! autoSignupEnabled: Boolean!
createdAt: Datetime! createdAt: Datetime!
updatedAt: Datetime! updatedAt: Datetime!
spMetadataUrl: String! testLoginUrl: String! @goField(forceResolver: true)
testLoginUrl: String!
attributeMappings: SAMLAttributeMappings! attributeMappings: SAMLAttributeMappings!
} }
@@ -640,7 +639,7 @@ input UpdateSAMLConfigurationInput {
idpSsoUrl: String idpSsoUrl: String
idpCertificate: String idpCertificate: String
autoSignupEnabled: Boolean autoSignupEnabled: Boolean
enforcementPolicy: SAMLEnforcementPolicy enforcementPolicy: SAMLEnforcementPolicy!
attributeMappings: SAMLAttributeMappingsInput attributeMappings: SAMLAttributeMappingsInput
} }

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
@@ -933,6 +934,7 @@ func (r *mutationResolver) UpdateSAMLConfiguration(ctx context.Context, input ty
IdPSsoURL: input.IdpSsoURL, IdPSsoURL: input.IdpSsoURL,
IdPCertificate: input.IdpCertificate, IdPCertificate: input.IdpCertificate,
AutoSignupEnabled: input.AutoSignupEnabled, AutoSignupEnabled: input.AutoSignupEnabled,
EnforcementPolicy: &input.EnforcementPolicy,
} }
if input.AttributeMappings != nil { 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 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: default:
return nil, fmt.Errorf("unsupported entity type: %d", id.EntityType()) 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")) 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. // TotalCount is the resolver for the totalCount field.
func (r *sAMLConfigurationConnectionResolver) TotalCount(ctx context.Context, obj *types.SAMLConfigurationConnection) (*int, error) { func (r *sAMLConfigurationConnectionResolver) TotalCount(ctx context.Context, obj *types.SAMLConfigurationConnection) (*int, error) {
switch obj.Resolver.(type) { switch obj.Resolver.(type) {
@@ -1314,6 +1337,11 @@ func (r *Resolver) PersonalAPIKeyConnection() schema.PersonalAPIKeyConnectionRes
// Query returns schema.QueryResolver implementation. // Query returns schema.QueryResolver implementation.
func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} } 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. // SAMLConfigurationConnection returns schema.SAMLConfigurationConnectionResolver implementation.
func (r *Resolver) SAMLConfigurationConnection() schema.SAMLConfigurationConnectionResolver { func (r *Resolver) SAMLConfigurationConnection() schema.SAMLConfigurationConnectionResolver {
return &sAMLConfigurationConnectionResolver{r} return &sAMLConfigurationConnectionResolver{r}
@@ -1336,6 +1364,7 @@ type mutationResolver struct{ *Resolver }
type organizationResolver struct{ *Resolver } type organizationResolver struct{ *Resolver }
type personalAPIKeyConnectionResolver struct{ *Resolver } type personalAPIKeyConnectionResolver struct{ *Resolver }
type queryResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
type sAMLConfigurationResolver struct{ *Resolver }
type sAMLConfigurationConnectionResolver struct{ *Resolver } type sAMLConfigurationConnectionResolver struct{ *Resolver }
type sessionResolver struct{ *Resolver } type sessionResolver struct{ *Resolver }
type sessionConnectionResolver struct{ *Resolver } type sessionConnectionResolver struct{ *Resolver }