From 7e22893fd261b3fe132502fa6be5c30a79b0a2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Wed, 31 Dec 2025 15:44:29 +0100 Subject: [PATCH] Fix saml config + org form permissions handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../iam/GeneralSettingsPageQuery.graphql.ts | 37 ++++++++- .../iam/InviteUserDialogMutation.graphql.ts | 22 ++++- .../iam/OrganizationFormFragment.graphql.ts | 18 ++++- .../SAMLConfigurationListFragment.graphql.ts | 32 +++++++- .../iam/SAMLSettingsPageQuery.graphql.ts | 81 ++++++++++++++++--- .../settings/GeneralSettingsPage.tsx | 8 +- .../settings/SAMLSettingsPage.tsx | 17 ++-- .../settings/_components/InviteUserDialog.tsx | 1 + .../settings/_components/OrganizationForm.tsx | 13 +-- .../_components/SAMLConfigurationList.tsx | 35 ++++---- 10 files changed, 199 insertions(+), 65 deletions(-) diff --git a/apps/console/src/__generated__/iam/GeneralSettingsPageQuery.graphql.ts b/apps/console/src/__generated__/iam/GeneralSettingsPageQuery.graphql.ts index d5383976a..d7b859cd8 100644 --- a/apps/console/src/__generated__/iam/GeneralSettingsPageQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/GeneralSettingsPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<7b9dd98afd5bdc994e7d2e422dec2cf2>> + * @generated SignedSource<<125407dec78c8db89ee91aa12a097cf0>> * @lightSyntaxTransform * @nogrep */ @@ -16,6 +16,7 @@ export type GeneralSettingsPageQuery$variables = { export type GeneralSettingsPageQuery$data = { readonly organization: { readonly __typename: "Organization"; + readonly canDelete: boolean; readonly id: string; readonly name: string; readonly " $fragmentSpreads": FragmentRefs<"OrganizationFormFragment">; @@ -65,6 +66,19 @@ v4 = { "kind": "ScalarField", "name": "name", "storageKey": null +}, +v5 = { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:organization:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:organization:delete\")" }; return { "fragment": { @@ -93,6 +107,7 @@ return { "field": (v4/*: any*/), "action": "THROW" }, + (v5/*: any*/), { "args": null, "kind": "FragmentSpread", @@ -131,6 +146,7 @@ return { "kind": "InlineFragment", "selections": [ (v4/*: any*/), + (v5/*: any*/), { "alias": null, "args": null, @@ -172,6 +188,19 @@ return { "kind": "ScalarField", "name": "headquarterAddress", "storageKey": null + }, + { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:organization:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:organization:update\")" } ], "type": "Organization", @@ -183,16 +212,16 @@ return { ] }, "params": { - "cacheID": "11f654f40301ba539b6d78ead3029308", + "cacheID": "0d8f14524ef2e72d138e5bb5a42a8f8d", "id": null, "metadata": {}, "name": "GeneralSettingsPageQuery", "operationKind": "query", - "text": "query GeneralSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n ...OrganizationFormFragment\n }\n id\n }\n}\n\nfragment OrganizationFormFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n}\n" + "text": "query GeneralSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n canDelete: permission(action: \"iam:organization:delete\")\n ...OrganizationFormFragment\n }\n id\n }\n}\n\nfragment OrganizationFormFragment on Organization {\n id\n name\n logoUrl\n horizontalLogoUrl\n description\n websiteUrl\n email\n headquarterAddress\n canUpdate: permission(action: \"iam:organization:update\")\n}\n" } }; })(); -(node as any).hash = "a83e24379e0772bbaf19bad92d60c6d4"; +(node as any).hash = "307473752dd5dc91caaa7536b7b3e69a"; export default node; diff --git a/apps/console/src/__generated__/iam/InviteUserDialogMutation.graphql.ts b/apps/console/src/__generated__/iam/InviteUserDialogMutation.graphql.ts index f4ca2f757..13d2ce902 100644 --- a/apps/console/src/__generated__/iam/InviteUserDialogMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/InviteUserDialogMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<84f3f3da89ad2bdfecd79b479bf74814>> + * @generated SignedSource<<4516a7a0c95bb6634bbe7d1b097cc5de>> * @lightSyntaxTransform * @nogrep */ @@ -26,6 +26,7 @@ export type InviteUserDialogMutation$data = { readonly invitationEdge: { readonly node: { readonly acceptedAt: any | null | undefined; + readonly canDelete: boolean; readonly createdAt: any; readonly email: any; readonly expiresAt: any; @@ -123,6 +124,19 @@ v3 = { "kind": "ScalarField", "name": "createdAt", "storageKey": null + }, + { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:invitation:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:invitation:delete\")" } ], "storageKey": null @@ -196,16 +210,16 @@ return { ] }, "params": { - "cacheID": "43ea730d742b7d259b50b50d3684147c", + "cacheID": "8d9e447c08739bef2460474225fb77bf", "id": null, "metadata": {}, "name": "InviteUserDialogMutation", "operationKind": "mutation", - "text": "mutation InviteUserDialogMutation(\n $input: InviteMemberInput!\n) {\n inviteMember(input: $input) {\n invitationEdge {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n }\n }\n }\n}\n" + "text": "mutation InviteUserDialogMutation(\n $input: InviteMemberInput!\n) {\n inviteMember(input: $input) {\n invitationEdge {\n node {\n id\n email\n fullName\n role\n expiresAt\n acceptedAt\n createdAt\n canDelete: permission(action: \"iam:invitation:delete\")\n }\n }\n }\n}\n" } }; })(); -(node as any).hash = "b45737075b750b79b3a90748cb2565f3"; +(node as any).hash = "dc2446f51b33dbab3a348e1250a25049"; export default node; diff --git a/apps/console/src/__generated__/iam/OrganizationFormFragment.graphql.ts b/apps/console/src/__generated__/iam/OrganizationFormFragment.graphql.ts index 99e3ecbe6..2eb28465a 100644 --- a/apps/console/src/__generated__/iam/OrganizationFormFragment.graphql.ts +++ b/apps/console/src/__generated__/iam/OrganizationFormFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<8deb33fbc8305caa3d3de954cf555c79>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -11,6 +11,7 @@ import { ReaderFragment } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type OrganizationFormFragment$data = { + readonly canUpdate: boolean; readonly description: string | null | undefined; readonly email: string | null | undefined; readonly headquarterAddress: string | null | undefined; @@ -91,12 +92,25 @@ const node: ReaderFragment = { "kind": "ScalarField", "name": "headquarterAddress", "storageKey": null + }, + { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:organization:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:organization:update\")" } ], "type": "Organization", "abstractKey": null }; -(node as any).hash = "0894093a550e5ba033a33b255b1bf2c7"; +(node as any).hash = "d1a518b08167fdd53901c4b842fa2e9c"; export default node; diff --git a/apps/console/src/__generated__/iam/SAMLConfigurationListFragment.graphql.ts b/apps/console/src/__generated__/iam/SAMLConfigurationListFragment.graphql.ts index f4f3b2959..6451afc94 100644 --- a/apps/console/src/__generated__/iam/SAMLConfigurationListFragment.graphql.ts +++ b/apps/console/src/__generated__/iam/SAMLConfigurationListFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<12fade1fd1f7f3ae53595970b6c2c47e>> + * @generated SignedSource<<6b22aeaed6a8da4374bb98a36e236cd2>> * @lightSyntaxTransform * @nogrep */ @@ -15,6 +15,8 @@ export type SAMLConfigurationListFragment$data = { readonly samlConfigurations: { readonly edges: ReadonlyArray<{ readonly node: { + readonly canDelete: boolean; + readonly canUpdate: boolean; readonly domainVerificationToken: string | null | undefined; readonly domainVerifiedAt: any | null | undefined; readonly emailDomain: string; @@ -118,6 +120,32 @@ const node: ReaderFragment = { "name": "testLoginUrl", "storageKey": null }, + { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:saml-configuration:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:saml-configuration:update\")" + }, + { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:saml-configuration:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:saml-configuration:delete\")" + }, { "alias": null, "args": null, @@ -175,6 +203,6 @@ const node: ReaderFragment = { "abstractKey": null }; -(node as any).hash = "79203ccae6f8e3a1967b463f0f3f3c76"; +(node as any).hash = "3adfb13b2a908c93602b219ef1d0cf50"; export default node; diff --git a/apps/console/src/__generated__/iam/SAMLSettingsPageQuery.graphql.ts b/apps/console/src/__generated__/iam/SAMLSettingsPageQuery.graphql.ts index 1eb6e5228..47782e492 100644 --- a/apps/console/src/__generated__/iam/SAMLSettingsPageQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/SAMLSettingsPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<57eb2489b4c62cb4b1df1a61bc135313>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -15,7 +15,13 @@ export type SAMLSettingsPageQuery$variables = { }; export type SAMLSettingsPageQuery$data = { readonly organization: { + readonly __typename: "Organization"; + readonly canCreateSAMLConfiguration: boolean; readonly " $fragmentSpreads": FragmentRefs<"SAMLConfigurationListFragment">; + } | { + // This will never be '%other', but we need some + // value in case none of the concrete values match. + readonly __typename: "%other"; }; }; export type SAMLSettingsPageQuery = { @@ -45,14 +51,27 @@ v2 = { "name": "__typename", "storageKey": null }, -v3 = [ +v3 = { + "alias": "canCreateSAMLConfiguration", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:saml-configuration:create" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:saml-configuration:create\")" +}, +v4 = [ { "kind": "Literal", "name": "first", "value": 1000 } ], -v4 = { +v5 = { "alias": null, "args": null, "kind": "ScalarField", @@ -76,10 +95,19 @@ return { "name": "node", "plural": false, "selections": [ + (v2/*: any*/), { - "args": null, - "kind": "FragmentSpread", - "name": "SAMLConfigurationListFragment" + "kind": "InlineFragment", + "selections": [ + (v3/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "SAMLConfigurationListFragment" + } + ], + "type": "Organization", + "abstractKey": null } ], "storageKey": null @@ -108,9 +136,10 @@ return { { "kind": "InlineFragment", "selections": [ + (v3/*: any*/), { "alias": null, - "args": (v3/*: any*/), + "args": (v4/*: any*/), "concreteType": "SAMLConfigurationConnection", "kind": "LinkedField", "name": "samlConfigurations", @@ -132,7 +161,7 @@ return { "name": "node", "plural": false, "selections": [ - (v4/*: any*/), + (v5/*: any*/), { "alias": null, "args": null, @@ -168,6 +197,32 @@ return { "name": "testLoginUrl", "storageKey": null }, + { + "alias": "canUpdate", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:saml-configuration:update" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:saml-configuration:update\")" + }, + { + "alias": "canDelete", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "iam:saml-configuration:delete" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"iam:saml-configuration:delete\")" + }, (v2/*: any*/) ], "storageKey": null @@ -212,7 +267,7 @@ return { }, { "alias": null, - "args": (v3/*: any*/), + "args": (v4/*: any*/), "filters": null, "handle": "connection", "key": "SAMLConfigurationListFragment_samlConfigurations", @@ -223,23 +278,23 @@ return { "type": "Organization", "abstractKey": null }, - (v4/*: any*/) + (v5/*: any*/) ], "storageKey": null } ] }, "params": { - "cacheID": "45740d8e7e6fba1ec2576fd62487e41e", + "cacheID": "1a2bfe52dd42459b83e7f99e9b790218", "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" + "text": "query SAMLSettingsPageQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n canCreateSAMLConfiguration: permission(action: \"iam:saml-configuration:create\")\n ...SAMLConfigurationListFragment\n }\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 canUpdate: permission(action: \"iam:saml-configuration:update\")\n canDelete: permission(action: \"iam:saml-configuration:delete\")\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" } }; })(); -(node as any).hash = "d2d6ef2a90f81a75df7ab0dd83ef52cf"; +(node as any).hash = "d0fd8fe9afb6ff650cce48c89eb83c46"; export default node; diff --git a/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPage.tsx b/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPage.tsx index a0c803dba..283063b92 100644 --- a/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPage.tsx +++ b/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPage.tsx @@ -1,7 +1,5 @@ import { useTranslate } from "@probo/i18n"; import { Button, Card, IconTrashCan } from "@probo/ui"; -import { use } from "react"; -import { PermissionsContext } from "/providers/PermissionsContext"; import { useNavigate } from "react-router"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { graphql } from "relay-runtime"; @@ -18,6 +16,7 @@ export const generalSettingsPageQuery = graphql` ... on Organization { id name @required(action: THROW) + canDelete: permission(action: "iam:organization:delete") ...OrganizationFormFragment } } @@ -42,9 +41,6 @@ export function GeneralSettingsPage(props: { const { __ } = useTranslate(); const navigate = useNavigate(); - const { isAuthorized } = use(PermissionsContext); - const canDelete = isAuthorized("Organization", "deleteOrganization"); - const { organization } = usePreloadedQuery( generalSettingsPageQuery, queryRef, @@ -80,7 +76,7 @@ export function GeneralSettingsPage(props: {
- {canDelete && ( + {organization.canDelete && (

{__("Danger Zone")} diff --git a/apps/console/src/pages/iam/organizations/settings/SAMLSettingsPage.tsx b/apps/console/src/pages/iam/organizations/settings/SAMLSettingsPage.tsx index 15325f80f..85485fd45 100644 --- a/apps/console/src/pages/iam/organizations/settings/SAMLSettingsPage.tsx +++ b/apps/console/src/pages/iam/organizations/settings/SAMLSettingsPage.tsx @@ -5,8 +5,7 @@ import { type PreloadedQuery, } from "react-relay"; import type { SAMLSettingsPageQuery } from "/__generated__/iam/SAMLSettingsPageQuery.graphql"; -import { Suspense, use, useState } from "react"; -import { PermissionsContext } from "/providers/PermissionsContext"; +import { Suspense, useState } from "react"; import { Breadcrumb, Button, Dialog, useDialogRef } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { SAMLConfigurationList } from "./_components/SAMLConfigurationList"; @@ -21,7 +20,13 @@ import { SAMLDomainVerifyDialog } from "./_components/SAMLDomainVerifyDialog"; export const samlSettingsPageQuery = graphql` query SAMLSettingsPageQuery($organizationId: ID!) { organization: node(id: $organizationId) @required(action: THROW) { - ...SAMLConfigurationListFragment + __typename + ... on Organization { + canCreateSAMLConfiguration: permission( + action: "iam:saml-configuration:create" + ) + ...SAMLConfigurationListFragment + } } } `; @@ -38,9 +43,11 @@ export function SAMLSettingsPage(props: { useState(); const { __ } = useTranslate(); - const { isAuthorized } = use(PermissionsContext); const { organization } = usePreloadedQuery(samlSettingsPageQuery, queryRef); + if (organization.__typename !== "Organization") { + throw new Error("invalid node type"); + } const [formQueryRef, loadFormQuery] = useQueryLoader(samlConfigurationFormQuery); @@ -70,7 +77,7 @@ export function SAMLSettingsPage(props: {

{__("SAML Single Sign-On")}

- {isAuthorized("Organization", "createSAMLConfiguration") && ( + {organization.canCreateSAMLConfiguration && ( diff --git a/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx b/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx index 95680d264..f18b0022a 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx +++ b/apps/console/src/pages/iam/organizations/settings/_components/InviteUserDialog.tsx @@ -44,6 +44,7 @@ const inviteMutation = graphql` expiresAt acceptedAt createdAt + canDelete: permission(action: "iam:invitation:delete") } } } diff --git a/apps/console/src/pages/iam/organizations/settings/_components/OrganizationForm.tsx b/apps/console/src/pages/iam/organizations/settings/_components/OrganizationForm.tsx index 07fd26cc7..b533db120 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/OrganizationForm.tsx +++ b/apps/console/src/pages/iam/organizations/settings/_components/OrganizationForm.tsx @@ -13,8 +13,7 @@ import { Textarea, useDialogRef, } from "@probo/ui"; -import { use, useState, type ChangeEventHandler } from "react"; -import { PermissionsContext } from "/providers/PermissionsContext"; +import { useState, type ChangeEventHandler } from "react"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import { useMutationWithToasts } from "/hooks/useMutationWithToasts"; import { graphql } from "relay-runtime"; @@ -33,6 +32,7 @@ const fragment = graphql` websiteUrl email headquarterAddress + canUpdate: permission(action: "iam:organization:update") } `; @@ -83,18 +83,13 @@ export function OrganizationForm(props: { const { __ } = useTranslate(); const deleteDialogRef = useDialogRef(); - const { isAuthorized } = use(PermissionsContext); - const canUpdate = isAuthorized("Organization", "updateOrganization"); - const [logoPreview, setLogoPreview] = useState(null); const [horizontalLogoPreview, setHorizontalLogoPreview] = useState< string | null >(null); - const organization = useFragment( - fragment, - fKey, - ); + const { canUpdate, ...organization } = + useFragment(fragment, fKey); const [updateOrganization, isUpdatingOrganization] = useMutationWithToasts( updateOrganizationMutation, diff --git a/apps/console/src/pages/iam/organizations/settings/_components/SAMLConfigurationList.tsx b/apps/console/src/pages/iam/organizations/settings/_components/SAMLConfigurationList.tsx index 72ff8947d..8f3442056 100644 --- a/apps/console/src/pages/iam/organizations/settings/_components/SAMLConfigurationList.tsx +++ b/apps/console/src/pages/iam/organizations/settings/_components/SAMLConfigurationList.tsx @@ -1,6 +1,4 @@ import { useTranslate } from "@probo/i18n"; -import { use } from "react"; -import { PermissionsContext } from "/providers/PermissionsContext"; import { Button, Card, @@ -37,6 +35,8 @@ const fragment = graphql` domainVerificationToken domainVerifiedAt testLoginUrl + canUpdate: permission(action: "iam:saml-configuration:update") + canDelete: permission(action: "iam:saml-configuration:delete") } } } @@ -63,7 +63,6 @@ export function SAMLConfigurationList(props: { const organizationId = useOrganizationId(); const { __ } = useTranslate(); - const { isAuthorized } = use(PermissionsContext); const confirm = useConfirm(); const [isCopied, copy] = useCopy(); @@ -197,10 +196,7 @@ export function SAMLConfigurationList(props: {
{config.domainVerifiedAt ? ( <> - {isAuthorized( - "SAMLConfiguration", - "updateSAMLConfiguration", - ) && ( + {config.canUpdate && ( )} - {isAuthorized("Organization", "deleteOrganization") && ( + {config.canDelete && ( - )} - {isAuthorized("Organization", "deleteOrganization") && ( + {config.canUpdate && !!config.domainVerificationToken && ( + + )} + {config.canDelete && (