From 7ce73bbcfa61b126ae9f2576360a27803f9de284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Wed, 21 Jan 2026 13:30:41 +0400 Subject: [PATCH] Fix SOA permissions handling + add scope conditions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../FrameworkDetailPageFragment.graphql.ts | 18 ++- .../core/FrameworkGraphNodeQuery.graphql.ts | 19 ++- ...teOfApplicabilityGraphNodeQuery.graphql.ts | 26 +++- .../iam/SidebarFragment.graphql.ts | 12 +- .../ViewerMembershipLayoutQuery.graphql.ts | 14 +- .../hooks/graph/StateOfApplicabilityGraph.ts | 1 + .../iam/organizations/_components/Sidebar.tsx | 4 +- .../frameworks/FrameworkDetailPage.tsx | 103 +++++++------ .../StateOfApplicabilityDetailPage.tsx | 18 ++- pkg/coredata/state_of_applicability.go | 114 +++++++------- .../state_of_applicability_control.go | 142 ++++++++++-------- pkg/probo/policies.go | 2 + 12 files changed, 272 insertions(+), 201 deletions(-) diff --git a/apps/console/src/__generated__/core/FrameworkDetailPageFragment.graphql.ts b/apps/console/src/__generated__/core/FrameworkDetailPageFragment.graphql.ts index 40b549ae8..33152ef72 100644 --- a/apps/console/src/__generated__/core/FrameworkDetailPageFragment.graphql.ts +++ b/apps/console/src/__generated__/core/FrameworkDetailPageFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<7f57391fcdf09c326854f1cc2e0008c2>> + * @generated SignedSource<<3db987ede3a3aa1dfa570e76a00ef224>> * @lightSyntaxTransform * @nogrep */ @@ -15,6 +15,7 @@ export type FrameworkDetailPageFragment$data = { readonly canCreateControl: boolean; readonly canDelete: boolean; readonly canExport: boolean; + readonly canGenerateSOA: boolean; readonly canUpdate: boolean; readonly controls: { readonly __id: string; @@ -140,6 +141,19 @@ return { "name": "permission", "storageKey": "permission(action:\"core:control:create\")" }, + { + "alias": "canGenerateSOA", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:framework:generate-state-of-applicability" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:framework:generate-state-of-applicability\")" + }, { "alias": null, "args": null, @@ -247,6 +261,6 @@ return { }; })(); -(node as any).hash = "c88994d5f1872bfe21cfe65e1911b76a"; +(node as any).hash = "a3eb8b9d80c69d7faa83eae88e3b986b"; export default node; diff --git a/apps/console/src/__generated__/core/FrameworkGraphNodeQuery.graphql.ts b/apps/console/src/__generated__/core/FrameworkGraphNodeQuery.graphql.ts index 1a33095dc..e710cf5d1 100644 --- a/apps/console/src/__generated__/core/FrameworkGraphNodeQuery.graphql.ts +++ b/apps/console/src/__generated__/core/FrameworkGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -189,6 +189,19 @@ return { "name": "permission", "storageKey": "permission(action:\"core:control:create\")" }, + { + "alias": "canGenerateSOA", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:framework:generate-state-of-applicability" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:framework:generate-state-of-applicability\")" + }, { "alias": null, "args": null, @@ -301,12 +314,12 @@ return { ] }, "params": { - "cacheID": "2a82ae6447b93b0fa6b570571694052b", + "cacheID": "58485d98106169267addab7729095131", "id": null, "metadata": {}, "name": "FrameworkGraphNodeQuery", "operationKind": "query", - "text": "query FrameworkGraphNodeQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n ... on Framework {\n id\n name\n ...FrameworkDetailPageFragment\n }\n id\n }\n}\n\nfragment FrameworkDetailPageFragment on Framework {\n id\n name\n description\n lightLogoURL\n darkLogoURL\n canExport: permission(action: \"core:franework:export\")\n canUpdate: permission(action: \"core:framework:update\")\n canDelete: permission(action: \"core:framework:delete\")\n canCreateControl: permission(action: \"core:control:create\")\n organization {\n name\n id\n }\n controls(first: 250, orderBy: {field: SECTION_TITLE, direction: ASC}) {\n edges {\n node {\n id\n sectionTitle\n name\n status\n exclusionJustification\n bestPractice\n }\n }\n }\n}\n" + "text": "query FrameworkGraphNodeQuery(\n $frameworkId: ID!\n) {\n node(id: $frameworkId) {\n __typename\n ... on Framework {\n id\n name\n ...FrameworkDetailPageFragment\n }\n id\n }\n}\n\nfragment FrameworkDetailPageFragment on Framework {\n id\n name\n description\n lightLogoURL\n darkLogoURL\n canExport: permission(action: \"core:franework:export\")\n canUpdate: permission(action: \"core:framework:update\")\n canDelete: permission(action: \"core:framework:delete\")\n canCreateControl: permission(action: \"core:control:create\")\n canGenerateSOA: permission(action: \"core:framework:generate-state-of-applicability\")\n organization {\n name\n id\n }\n controls(first: 250, orderBy: {field: SECTION_TITLE, direction: ASC}) {\n edges {\n node {\n id\n sectionTitle\n name\n status\n exclusionJustification\n bestPractice\n }\n }\n }\n}\n" } }; })(); diff --git a/apps/console/src/__generated__/core/StateOfApplicabilityGraphNodeQuery.graphql.ts b/apps/console/src/__generated__/core/StateOfApplicabilityGraphNodeQuery.graphql.ts index 86911e9f3..b07c23f98 100644 --- a/apps/console/src/__generated__/core/StateOfApplicabilityGraphNodeQuery.graphql.ts +++ b/apps/console/src/__generated__/core/StateOfApplicabilityGraphNodeQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<4c350f48aeb8f51c9716aed4eeadc735>> * @lightSyntaxTransform * @nogrep */ @@ -16,6 +16,7 @@ export type StateOfApplicabilityGraphNodeQuery$variables = { export type StateOfApplicabilityGraphNodeQuery$data = { readonly node: { readonly canDelete?: boolean; + readonly canExport?: boolean; readonly canUpdate?: boolean; readonly createdAt?: string; readonly id?: string; @@ -122,6 +123,19 @@ v9 = { "storageKey": "permission(action:\"core:state-of-applicability:delete\")" }, v10 = { + "alias": "canExport", + "args": [ + { + "kind": "Literal", + "name": "action", + "value": "core:state-of-applicability:export" + } + ], + "kind": "ScalarField", + "name": "permission", + "storageKey": "permission(action:\"core:state-of-applicability:export\")" +}, +v11 = { "alias": null, "args": null, "concreteType": "Organization", @@ -133,7 +147,7 @@ v10 = { ], "storageKey": null }, -v11 = { +v12 = { "alias": null, "args": null, "concreteType": "People", @@ -180,6 +194,7 @@ return { (v9/*: any*/), (v10/*: any*/), (v11/*: any*/), + (v12/*: any*/), { "args": null, "kind": "FragmentSpread", @@ -230,6 +245,7 @@ return { (v9/*: any*/), (v10/*: any*/), (v11/*: any*/), + (v12/*: any*/), { "alias": "controlsInfo", "args": [ @@ -386,16 +402,16 @@ return { ] }, "params": { - "cacheID": "6189c0ca41e3327629d744a658cbe6f1", + "cacheID": "529e47a135ba24716b749998d1181d9a", "id": null, "metadata": {}, "name": "StateOfApplicabilityGraphNodeQuery", "operationKind": "query", - "text": "query StateOfApplicabilityGraphNodeQuery(\n $stateOfApplicabilityId: ID!\n) {\n node(id: $stateOfApplicabilityId) {\n __typename\n ... on StateOfApplicability {\n id\n name\n sourceId\n snapshotId\n createdAt\n updatedAt\n canUpdate: permission(action: \"core:state-of-applicability:update\")\n canDelete: permission(action: \"core:state-of-applicability:delete\")\n organization {\n id\n }\n owner {\n id\n fullName\n }\n ...StateOfApplicabilityControlsTabFragment\n }\n id\n }\n}\n\nfragment StateOfApplicabilityControlsTabFragment on StateOfApplicability {\n id\n controlsInfo: controls(first: 0) {\n totalCount\n }\n canCreateStateOfApplicabilityControlMapping: permission(action: \"core:state-of-applicability-control-mapping:create\")\n canDeleteStateOfApplicabilityControlMapping: permission(action: \"core:state-of-applicability-control-mapping:delete\")\n availableControls {\n controlId\n sectionTitle\n name\n frameworkId\n frameworkName\n organizationId\n stateOfApplicabilityId\n applicability\n justification\n bestPractice\n regulatory\n contractual\n riskAssessment\n }\n}\n" + "text": "query StateOfApplicabilityGraphNodeQuery(\n $stateOfApplicabilityId: ID!\n) {\n node(id: $stateOfApplicabilityId) {\n __typename\n ... on StateOfApplicability {\n id\n name\n sourceId\n snapshotId\n createdAt\n updatedAt\n canUpdate: permission(action: \"core:state-of-applicability:update\")\n canDelete: permission(action: \"core:state-of-applicability:delete\")\n canExport: permission(action: \"core:state-of-applicability:export\")\n organization {\n id\n }\n owner {\n id\n fullName\n }\n ...StateOfApplicabilityControlsTabFragment\n }\n id\n }\n}\n\nfragment StateOfApplicabilityControlsTabFragment on StateOfApplicability {\n id\n controlsInfo: controls(first: 0) {\n totalCount\n }\n canCreateStateOfApplicabilityControlMapping: permission(action: \"core:state-of-applicability-control-mapping:create\")\n canDeleteStateOfApplicabilityControlMapping: permission(action: \"core:state-of-applicability-control-mapping:delete\")\n availableControls {\n controlId\n sectionTitle\n name\n frameworkId\n frameworkName\n organizationId\n stateOfApplicabilityId\n applicability\n justification\n bestPractice\n regulatory\n contractual\n riskAssessment\n }\n}\n" } }; })(); -(node as any).hash = "1a1f3480b9c66597d146c48ed1b00bd1"; +(node as any).hash = "5ea300f4dd399cafd7985072c2013dbe"; export default node; diff --git a/apps/console/src/__generated__/iam/SidebarFragment.graphql.ts b/apps/console/src/__generated__/iam/SidebarFragment.graphql.ts index 38d346843..5ab91a5fd 100644 --- a/apps/console/src/__generated__/iam/SidebarFragment.graphql.ts +++ b/apps/console/src/__generated__/iam/SidebarFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<63f008ba2b41fba49ba0c3145874aa72>> * @lightSyntaxTransform * @nogrep */ @@ -76,12 +76,12 @@ const node: ReaderFragment = { { "kind": "Literal", "name": "action", - "value": "core:measures:list" + "value": "core:measure:list" } ], "kind": "ScalarField", "name": "permission", - "storageKey": "permission(action:\"core:measures:list\")" + "storageKey": "permission(action:\"core:measure:list\")" }, { "alias": "canListRisks", @@ -102,12 +102,12 @@ const node: ReaderFragment = { { "kind": "Literal", "name": "action", - "value": "core:frameworks:list" + "value": "core:framework:list" } ], "kind": "ScalarField", "name": "permission", - "storageKey": "permission(action:\"core:frameworks:list\")" + "storageKey": "permission(action:\"core:framework:list\")" }, { "alias": "canListPeople", @@ -309,6 +309,6 @@ const node: ReaderFragment = { "abstractKey": null }; -(node as any).hash = "3a7071a1d9ed4ffc115f5bc22e283a50"; +(node as any).hash = "6d640139997b2c7561699c1efb668650"; export default node; diff --git a/apps/console/src/__generated__/iam/ViewerMembershipLayoutQuery.graphql.ts b/apps/console/src/__generated__/iam/ViewerMembershipLayoutQuery.graphql.ts index 898a35ad1..96e704697 100644 --- a/apps/console/src/__generated__/iam/ViewerMembershipLayoutQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/ViewerMembershipLayoutQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<<0f5d74875ad984071d92f1d0e3cfce12>> * @lightSyntaxTransform * @nogrep */ @@ -352,12 +352,12 @@ return { { "kind": "Literal", "name": "action", - "value": "core:measures:list" + "value": "core:measure:list" } ], "kind": "ScalarField", "name": "permission", - "storageKey": "permission(action:\"core:measures:list\")" + "storageKey": "permission(action:\"core:measure:list\")" }, { "alias": "canListRisks", @@ -378,12 +378,12 @@ return { { "kind": "Literal", "name": "action", - "value": "core:frameworks:list" + "value": "core:framework:list" } ], "kind": "ScalarField", "name": "permission", - "storageKey": "permission(action:\"core:frameworks:list\")" + "storageKey": "permission(action:\"core:framework:list\")" }, { "alias": "canListPeople", @@ -618,12 +618,12 @@ return { ] }, "params": { - "cacheID": "d7e0b080876472d059c97bf57818c697", + "cacheID": "f380d0fe83b6d660c61c379af4d2eb7d", "id": null, "metadata": {}, "name": "ViewerMembershipLayoutQuery", "operationKind": "query", - "text": "query ViewerMembershipLayoutQuery(\n $organizationId: ID!\n $hideSidebar: Boolean!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...MembershipsDropdown_organizationFragment\n ...ViewerMembershipDropdownFragment\n ...SidebarFragment @skip(if: $hideSidebar)\n viewerMembership {\n role\n profile {\n fullName\n id\n }\n id\n }\n }\n id\n }\n viewer {\n email\n ...MembershipsDropdown_viewerFragment\n pendingInvitations {\n totalCount\n }\n id\n }\n}\n\nfragment MembershipsDropdown_organizationFragment on Organization {\n name\n}\n\nfragment MembershipsDropdown_viewerFragment on Identity {\n pendingInvitations {\n totalCount\n }\n}\n\nfragment SidebarFragment on Organization {\n canListMeetings: permission(action: \"core:meeting:list\")\n canListTasks: permission(action: \"core:task:list\")\n canListMeasures: permission(action: \"core:measures:list\")\n canListRisks: permission(action: \"core:risk:list\")\n canListFrameworks: permission(action: \"core:frameworks:list\")\n canListPeople: permission(action: \"core:people:list\")\n canListVendors: permission(action: \"core:vendor:list\")\n canListDocuments: permission(action: \"core:document:list\")\n canListAssets: permission(action: \"core:asset:list\")\n canListData: permission(action: \"core:datum:list\")\n canListAudits: permission(action: \"core:audit:list\")\n canListNonconformities: permission(action: \"core:nonconformity:list\")\n canListObligations: permission(action: \"core:obligation:list\")\n canListContinualImprovements: permission(action: \"core:continual-improvement:list\")\n canListProcessingActivities: permission(action: \"core:processing-activity:list\")\n canListRightsRequests: permission(action: \"core:rights-request:list\")\n canListSnapshots: permission(action: \"core:snapshot:list\")\n canGetTrustCenter: permission(action: \"core:trust-center:get\")\n canUpdateOrganization: permission(action: \"iam:organization:update\")\n canListStatesOfApplicability: permission(action: \"core:state-of-applicability:list\")\n}\n\nfragment ViewerMembershipDropdownFragment on Organization {\n viewerMembership {\n identity {\n email\n canListAPIKeys: permission(action: \"iam:personal-api-key:list\")\n id\n }\n profile {\n fullName\n id\n }\n id\n }\n}\n" + "text": "query ViewerMembershipLayoutQuery(\n $organizationId: ID!\n $hideSidebar: Boolean!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...MembershipsDropdown_organizationFragment\n ...ViewerMembershipDropdownFragment\n ...SidebarFragment @skip(if: $hideSidebar)\n viewerMembership {\n role\n profile {\n fullName\n id\n }\n id\n }\n }\n id\n }\n viewer {\n email\n ...MembershipsDropdown_viewerFragment\n pendingInvitations {\n totalCount\n }\n id\n }\n}\n\nfragment MembershipsDropdown_organizationFragment on Organization {\n name\n}\n\nfragment MembershipsDropdown_viewerFragment on Identity {\n pendingInvitations {\n totalCount\n }\n}\n\nfragment SidebarFragment on Organization {\n canListMeetings: permission(action: \"core:meeting:list\")\n canListTasks: permission(action: \"core:task:list\")\n canListMeasures: permission(action: \"core:measure:list\")\n canListRisks: permission(action: \"core:risk:list\")\n canListFrameworks: permission(action: \"core:framework:list\")\n canListPeople: permission(action: \"core:people:list\")\n canListVendors: permission(action: \"core:vendor:list\")\n canListDocuments: permission(action: \"core:document:list\")\n canListAssets: permission(action: \"core:asset:list\")\n canListData: permission(action: \"core:datum:list\")\n canListAudits: permission(action: \"core:audit:list\")\n canListNonconformities: permission(action: \"core:nonconformity:list\")\n canListObligations: permission(action: \"core:obligation:list\")\n canListContinualImprovements: permission(action: \"core:continual-improvement:list\")\n canListProcessingActivities: permission(action: \"core:processing-activity:list\")\n canListRightsRequests: permission(action: \"core:rights-request:list\")\n canListSnapshots: permission(action: \"core:snapshot:list\")\n canGetTrustCenter: permission(action: \"core:trust-center:get\")\n canUpdateOrganization: permission(action: \"iam:organization:update\")\n canListStatesOfApplicability: permission(action: \"core:state-of-applicability:list\")\n}\n\nfragment ViewerMembershipDropdownFragment on Organization {\n viewerMembership {\n identity {\n email\n canListAPIKeys: permission(action: \"iam:personal-api-key:list\")\n id\n }\n profile {\n fullName\n id\n }\n id\n }\n}\n" } }; })(); diff --git a/apps/console/src/hooks/graph/StateOfApplicabilityGraph.ts b/apps/console/src/hooks/graph/StateOfApplicabilityGraph.ts index a6bbfd000..6c2ac991c 100644 --- a/apps/console/src/hooks/graph/StateOfApplicabilityGraph.ts +++ b/apps/console/src/hooks/graph/StateOfApplicabilityGraph.ts @@ -204,6 +204,7 @@ export const stateOfApplicabilityNodeQuery = graphql` updatedAt canUpdate: permission(action: "core:state-of-applicability:update") canDelete: permission(action: "core:state-of-applicability:delete") + canExport: permission(action: "core:state-of-applicability:export") organization { id } diff --git a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx index 2c031dea0..1278a58c2 100644 --- a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx +++ b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx @@ -31,9 +31,9 @@ const fragment = graphql` fragment SidebarFragment on Organization { canListMeetings: permission(action: "core:meeting:list") canListTasks: permission(action: "core:task:list") - canListMeasures: permission(action: "core:measures:list") + canListMeasures: permission(action: "core:measure:list") canListRisks: permission(action: "core:risk:list") - canListFrameworks: permission(action: "core:frameworks:list") + canListFrameworks: permission(action: "core:framework:list") canListPeople: permission(action: "core:people:list") canListVendors: permission(action: "core:vendor:list") canListDocuments: permission(action: "core:document:list") diff --git a/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx b/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx index 9e3624fde..3e418238d 100644 --- a/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx +++ b/apps/console/src/pages/organizations/frameworks/FrameworkDetailPage.tsx @@ -43,6 +43,7 @@ const frameworkDetailFragment = graphql` canUpdate: permission(action: "core:framework:update") canDelete: permission(action: "core:framework:delete") canCreateControl: permission(action: "core:control:create") + canGenerateSOA: permission(action: "core:framework:generate-state-of-applicability") organization { name } @@ -151,6 +152,8 @@ export default function FrameworkDetailPage(props: Props) { ); } + const hasAnyAction = framework.canExport || framework.canDelete || framework.canGenerateSOA + return (
)} - - { - generateFrameworkStateOfApplicability({ - variables: { frameworkId: framework.id }, - onCompleted: (data) => { - if ( - data - .generateFrameworkStateOfApplicability - ?.data - ) { - const link = - window.document.createElement("a"); - link.href = - data.generateFrameworkStateOfApplicability.data; - link.download = `${framework.organization.name}-${framework.name}-SOA.xlsx`; - window.document.body.appendChild(link); - link.click(); - window.document.body.removeChild(link); - } - }, - }); - }} - > - {__("Download SOA")} - - { - exportFramework({ - variables: { frameworkId: framework.id }, - }); - }} - > - {__("Export Framework")} - - {framework.canDelete && ( - - {__("Delete")} - - )} - + {hasAnyAction && ( + + {framework.canGenerateSOA && + { + generateFrameworkStateOfApplicability({ + variables: { frameworkId: framework.id }, + onCompleted: (data) => { + if ( + data + .generateFrameworkStateOfApplicability + ?.data + ) { + const link = + window.document.createElement("a"); + link.href = + data.generateFrameworkStateOfApplicability.data; + link.download = `${framework.organization.name}-${framework.name}-SOA.xlsx`; + window.document.body.appendChild(link); + link.click(); + window.document.body.removeChild(link); + } + }, + }); + }} + > + {__("Download SOA")} + + } + {framework.canExport && + { + exportFramework({ + variables: { frameworkId: framework.id }, + }); + }} + > + {__("Export Framework")} + + } + {framework.canDelete && ( + + {__("Delete")} + + )} + + )}
{__("Requirement categories")} diff --git a/apps/console/src/pages/organizations/states-of-applicability/StateOfApplicabilityDetailPage.tsx b/apps/console/src/pages/organizations/states-of-applicability/StateOfApplicabilityDetailPage.tsx index 3d70bdbf2..0509fa108 100644 --- a/apps/console/src/pages/organizations/states-of-applicability/StateOfApplicabilityDetailPage.tsx +++ b/apps/console/src/pages/organizations/states-of-applicability/StateOfApplicabilityDetailPage.tsx @@ -279,14 +279,16 @@ export default function StateOfApplicabilityDetailPage(props: Props) { ) } > - + {stateOfApplicability.canExport && ( + + )} {canDelete && (