Fix audit pages permission handling

Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
Émile Ré
2025-12-31 15:59:33 +01:00
committed by Bryan Frimin
parent a59e62782b
commit da3f9c0692
8 changed files with 206 additions and 50 deletions

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<b6236fab1ef470add01b9d9cc7527caa>>
* @generated SignedSource<<5cef509d4e44e0e0064c79fc7c3bf2e3>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -28,6 +28,8 @@ export type AuditGraphCreateMutation$data = {
readonly createAudit: {
readonly auditEdge: {
readonly node: {
readonly canDelete: boolean;
readonly canUpdate: boolean;
readonly createdAt: any;
readonly framework: {
readonly id: string;
@@ -160,6 +162,32 @@ v5 = {
"kind": "ScalarField",
"name": "createdAt",
"storageKey": null
},
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:delete\")"
}
],
"storageKey": null
@@ -233,16 +261,16 @@ return {
]
},
"params": {
"cacheID": "04d44dfd419c2cde11b0138ca175203f",
"cacheID": "d5155df01b8fe2fb74b49b953d49aed7",
"id": null,
"metadata": {},
"name": "AuditGraphCreateMutation",
"operationKind": "mutation",
"text": "mutation AuditGraphCreateMutation(\n $input: CreateAuditInput!\n) {\n createAudit(input: $input) {\n auditEdge {\n node {\n id\n name\n validFrom\n validUntil\n report {\n id\n filename\n }\n state\n framework {\n id\n name\n }\n createdAt\n }\n }\n }\n}\n"
"text": "mutation AuditGraphCreateMutation(\n $input: CreateAuditInput!\n) {\n createAudit(input: $input) {\n auditEdge {\n node {\n id\n name\n validFrom\n validUntil\n report {\n id\n filename\n }\n state\n framework {\n id\n name\n }\n createdAt\n canUpdate: permission(action: \"core:audit:update\")\n canDelete: permission(action: \"core:audit:delete\")\n }\n }\n }\n}\n"
}
};
})();
(node as any).hash = "4d014b16e02353df354085a151ccfe06";
(node as any).hash = "dcca4ebf33522ad6ef45456612fad03f";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<145565eb150c67f2f1b3c70fe6f6e748>>
* @generated SignedSource<<a866ac360421f2cca952ab1305c3de0b>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -15,6 +15,7 @@ export type AuditGraphListQuery$variables = {
};
export type AuditGraphListQuery$data = {
readonly node: {
readonly canCreateAudit?: boolean;
readonly " $fragmentSpreads": FragmentRefs<"AuditsPageFragment">;
};
};
@@ -39,27 +40,40 @@ v1 = [
}
],
v2 = {
"alias": "canCreateAudit",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:create"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:create\")"
},
v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
v3 = {
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
v4 = [
v5 = [
{
"kind": "Literal",
"name": "first",
"value": 10
}
],
v5 = {
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
@@ -84,6 +98,7 @@ return {
{
"kind": "InlineFragment",
"selections": [
(v2/*: any*/),
{
"args": null,
"kind": "FragmentSpread",
@@ -114,14 +129,15 @@ return {
"name": "node",
"plural": false,
"selections": [
(v2/*: any*/),
(v3/*: any*/),
(v4/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v2/*: any*/),
{
"alias": null,
"args": (v4/*: any*/),
"args": (v5/*: any*/),
"concreteType": "AuditConnection",
"kind": "LinkedField",
"name": "audits",
@@ -143,8 +159,8 @@ return {
"name": "node",
"plural": false,
"selections": [
(v3/*: any*/),
(v5/*: any*/),
(v4/*: any*/),
(v6/*: any*/),
{
"alias": null,
"args": null,
@@ -167,7 +183,7 @@ return {
"name": "report",
"plural": false,
"selections": [
(v3/*: any*/),
(v4/*: any*/),
{
"alias": null,
"args": null,
@@ -193,8 +209,8 @@ return {
"name": "framework",
"plural": false,
"selections": [
(v3/*: any*/),
(v5/*: any*/)
(v4/*: any*/),
(v6/*: any*/)
],
"storageKey": null
},
@@ -205,7 +221,33 @@ return {
"name": "createdAt",
"storageKey": null
},
(v2/*: any*/)
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:delete\")"
},
(v3/*: any*/)
],
"storageKey": null
},
@@ -275,7 +317,7 @@ return {
},
{
"alias": null,
"args": (v4/*: any*/),
"args": (v5/*: any*/),
"filters": [
"orderBy"
],
@@ -294,16 +336,16 @@ return {
]
},
"params": {
"cacheID": "d96d2332efb5f43e2dcb09669173ad7e",
"cacheID": "441524ada7aa614fb611f3d64812314f",
"id": null,
"metadata": {},
"name": "AuditGraphListQuery",
"operationKind": "query",
"text": "query AuditGraphListQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n ...AuditsPageFragment\n }\n id\n }\n}\n\nfragment AuditsPageFragment on Organization {\n audits(first: 10) {\n edges {\n node {\n id\n name\n validFrom\n validUntil\n report {\n id\n filename\n }\n state\n framework {\n id\n name\n }\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
"text": "query AuditGraphListQuery(\n $organizationId: ID!\n) {\n node(id: $organizationId) {\n __typename\n ... on Organization {\n canCreateAudit: permission(action: \"core:audit:create\")\n ...AuditsPageFragment\n }\n id\n }\n}\n\nfragment AuditsPageFragment on Organization {\n audits(first: 10) {\n edges {\n node {\n id\n name\n validFrom\n validUntil\n report {\n id\n filename\n }\n state\n framework {\n id\n name\n }\n createdAt\n canUpdate: permission(action: \"core:audit:update\")\n canDelete: permission(action: \"core:audit:delete\")\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "3a082303ae15c8982a08c3bae8312846";
(node as any).hash = "fc989e08b0d6411aac7ca6b1a0125edc";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<c802dc4c6e5b63e48b0050dfcf13b4b4>>
* @generated SignedSource<<c8e8bf84dd8523199ec6e0d1eed7eb01>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -15,6 +15,8 @@ export type AuditGraphNodeQuery$variables = {
};
export type AuditGraphNodeQuery$data = {
readonly node: {
readonly canDelete?: boolean;
readonly canUpdate?: boolean;
readonly createdAt?: any;
readonly framework?: {
readonly darkLogoURL: string | null | undefined;
@@ -199,6 +201,32 @@ v12 = {
"kind": "ScalarField",
"name": "updatedAt",
"storageKey": null
},
v13 = {
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:update\")"
},
v14 = {
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:delete\")"
};
return {
"fragment": {
@@ -228,7 +256,9 @@ return {
(v10/*: any*/),
(v11/*: any*/),
(v6/*: any*/),
(v12/*: any*/)
(v12/*: any*/),
(v13/*: any*/),
(v14/*: any*/)
],
"type": "Audit",
"abstractKey": null
@@ -274,7 +304,9 @@ return {
(v10/*: any*/),
(v11/*: any*/),
(v6/*: any*/),
(v12/*: any*/)
(v12/*: any*/),
(v13/*: any*/),
(v14/*: any*/)
],
"type": "Audit",
"abstractKey": null
@@ -285,16 +317,16 @@ return {
]
},
"params": {
"cacheID": "af5fde4e1522c98b908630fdf1451ae9",
"cacheID": "c9d41c45b625f17ccf291ae2c027a027",
"id": null,
"metadata": {},
"name": "AuditGraphNodeQuery",
"operationKind": "query",
"text": "query AuditGraphNodeQuery(\n $auditId: ID!\n) {\n node(id: $auditId) {\n __typename\n ... on Audit {\n id\n name\n validFrom\n validUntil\n report {\n id\n filename\n mimeType\n size\n downloadUrl\n createdAt\n }\n reportUrl\n state\n framework {\n id\n name\n lightLogoURL\n darkLogoURL\n }\n organization {\n id\n name\n }\n createdAt\n updatedAt\n }\n id\n }\n}\n"
"text": "query AuditGraphNodeQuery(\n $auditId: ID!\n) {\n node(id: $auditId) {\n __typename\n ... on Audit {\n id\n name\n validFrom\n validUntil\n report {\n id\n filename\n mimeType\n size\n downloadUrl\n createdAt\n }\n reportUrl\n state\n framework {\n id\n name\n lightLogoURL\n darkLogoURL\n }\n organization {\n id\n name\n }\n createdAt\n updatedAt\n canUpdate: permission(action: \"core:audit:update\")\n canDelete: permission(action: \"core:audit:delete\")\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "db92119c6d6f000963a0ff3d0fb74dfd";
(node as any).hash = "3195b5564b2cf8a21536ebf89f0f5c82";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<0010416e8a5bda346d415bd80fd70dc8>>
* @generated SignedSource<<36758d66ccb0304442a2a59898fe95a0>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -266,6 +266,32 @@ return {
"name": "createdAt",
"storageKey": null
},
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:delete\")"
},
(v8/*: any*/)
],
"storageKey": null
@@ -355,16 +381,16 @@ return {
]
},
"params": {
"cacheID": "9d185e504f8c34d5c3fd7211d4e8a2a2",
"cacheID": "7804de168f5c2484d6beb2f4d19287b0",
"id": null,
"metadata": {},
"name": "AuditsListQuery",
"operationKind": "query",
"text": "query AuditsListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 10\n $last: Int = null\n $orderBy: AuditOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...AuditsPageFragment_sdb03\n id\n }\n}\n\nfragment AuditsPageFragment_sdb03 on Organization {\n audits(first: $first, after: $after, last: $last, before: $before, orderBy: $orderBy) {\n edges {\n node {\n id\n name\n validFrom\n validUntil\n report {\n id\n filename\n }\n state\n framework {\n id\n name\n }\n createdAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
"text": "query AuditsListQuery(\n $after: CursorKey = null\n $before: CursorKey = null\n $first: Int = 10\n $last: Int = null\n $orderBy: AuditOrder = null\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...AuditsPageFragment_sdb03\n id\n }\n}\n\nfragment AuditsPageFragment_sdb03 on Organization {\n audits(first: $first, after: $after, last: $last, before: $before, orderBy: $orderBy) {\n edges {\n node {\n id\n name\n validFrom\n validUntil\n report {\n id\n filename\n }\n state\n framework {\n id\n name\n }\n createdAt\n canUpdate: permission(action: \"core:audit:update\")\n canDelete: permission(action: \"core:audit:delete\")\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n hasPreviousPage\n startCursor\n }\n }\n id\n}\n"
}
};
})();
(node as any).hash = "be8d1f1d8015c16539886bc79c8026f7";
(node as any).hash = "4013ec4e657ef7fb9d13afbd572ea9e5";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<8c09163f7cf7b3953aed77791b0d3dd1>>
* @generated SignedSource<<075733c8694be657990e9fb8399c1ae6>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -16,6 +16,8 @@ export type AuditsPageFragment$data = {
readonly __id: string;
readonly edges: ReadonlyArray<{
readonly node: {
readonly canDelete: boolean;
readonly canUpdate: boolean;
readonly createdAt: any;
readonly framework: {
readonly id: string;
@@ -215,6 +217,32 @@ return {
"name": "createdAt",
"storageKey": null
},
{
"alias": "canUpdate",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:update"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:update\")"
},
{
"alias": "canDelete",
"args": [
{
"kind": "Literal",
"name": "action",
"value": "core:audit:delete"
}
],
"kind": "ScalarField",
"name": "permission",
"storageKey": "permission(action:\"core:audit:delete\")"
},
{
"alias": null,
"args": null,
@@ -296,6 +324,6 @@ return {
};
})();
(node as any).hash = "be8d1f1d8015c16539886bc79c8026f7";
(node as any).hash = "4013ec4e657ef7fb9d13afbd572ea9e5";
export default node;

View File

@@ -9,6 +9,7 @@ export const auditsQuery = graphql`
query AuditGraphListQuery($organizationId: ID!) {
node(id: $organizationId) {
... on Organization {
canCreateAudit: permission(action: "core:audit:create")
...AuditsPageFragment
}
}
@@ -45,6 +46,8 @@ export const auditNodeQuery = graphql`
}
createdAt
updatedAt
canUpdate: permission(action: "core:audit:update")
canDelete: permission(action: "core:audit:delete")
}
}
}
@@ -72,6 +75,8 @@ export const createAuditMutation = graphql`
name
}
createdAt
canUpdate: permission(action: "core:audit:update")
canDelete: permission(action: "core:audit:delete")
}
}
}
@@ -115,7 +120,7 @@ export const deleteAuditMutation = graphql`
export const useDeleteAudit = (
audit: { id: string; framework: { name: string } },
connectionId: string,
onSuccess?: () => void
onSuccess?: () => void,
) => {
const { __ } = useTranslate();
const [mutate] = useMutationWithToasts(deleteAuditMutation, {
@@ -140,11 +145,11 @@ export const useDeleteAudit = (
{
message: sprintf(
__(
"This will permanently delete the audit for %s. This action cannot be undone."
"This will permanently delete the audit for %s. This action cannot be undone.",
),
audit.framework.name
audit.framework.name,
),
}
},
);
};
};

View File

@@ -44,8 +44,6 @@ import {
type GraphQLError,
} from "@probo/helpers";
import type { AuditGraphNodeQuery } from "/__generated__/core/AuditGraphNodeQuery.graphql";
import { use } from "react";
import { PermissionsContext } from "/providers/PermissionsContext";
import { useNavigate } from "react-router";
const updateAuditSchema = z.object({
@@ -73,7 +71,6 @@ export default function AuditDetailsPage(props: Props) {
const auditEntry = audit.node;
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const navigate = useNavigate();
const deleteAudit = useDeleteAudit(
@@ -178,7 +175,7 @@ export default function AuditDetailsPage(props: Props) {
</Badge>
</div>
<ActionDropdown variant="secondary">
{isAuthorized("Audit", "deleteAudit") && (
{auditEntry.canDelete && (
<DropdownItem
variant="danger"
icon={IconTrashCan}
@@ -218,7 +215,7 @@ export default function AuditDetailsPage(props: Props) {
</Field>
<div className="flex justify-end">
{formState.isDirty && isAuthorized("Audit", "updateAudit") && (
{formState.isDirty && auditEntry.canUpdate && (
<Button type="submit" disabled={formState.isSubmitting}>
{formState.isSubmitting ? __("Updating...") : __("Update")}
</Button>

View File

@@ -35,8 +35,6 @@ import type {
AuditsPageFragment$key,
} from "/__generated__/core/AuditsPageFragment.graphql";
import { SortableTable } from "/components/SortableTable";
import { PermissionsContext } from "/providers/PermissionsContext";
import { use } from "react";
const paginatedAuditsFragment = graphql`
fragment AuditsPageFragment on Organization
@@ -72,6 +70,8 @@ const paginatedAuditsFragment = graphql`
name
}
createdAt
canUpdate: permission(action: "core:audit:update")
canDelete: permission(action: "core:audit:delete")
}
}
}
@@ -87,7 +87,6 @@ type Props = {
export default function AuditsPage(props: Props) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();
const { isAuthorized } = use(PermissionsContext);
const data = usePreloadedQuery(auditsQuery, props.queryRef);
const pagination = usePaginationFragment(
@@ -99,9 +98,9 @@ export default function AuditsPage(props: Props) {
usePageTitle(__("Audits"));
const hasAnyAction =
isAuthorized("Audit", "updateAudit") ||
isAuthorized("Audit", "deleteAudit");
const hasAnyAction = audits.some(
(audit) => audit.canDelete || audit.canUpdate,
);
return (
<div className="space-y-6">
@@ -111,7 +110,7 @@ export default function AuditsPage(props: Props) {
"Manage your organization's compliance audits and their progress.",
)}
>
{isAuthorized("Organization", "createAudit") && (
{data.node.canCreateAudit && (
<CreateAuditDialog
connection={connectionId}
organizationId={organizationId}
@@ -159,7 +158,6 @@ function AuditRow({
const organizationId = useOrganizationId();
const { __ } = useTranslate();
const deleteAudit = useDeleteAudit(entry, connectionId);
const { isAuthorized } = use(PermissionsContext);
return (
<Tr to={`/organizations/${organizationId}/audits/${entry.id}`}>
@@ -184,7 +182,7 @@ function AuditRow({
{hasAnyAction && (
<Td noLink width={50} className="text-end">
<ActionDropdown>
{isAuthorized("Audit", "deleteAudit") && (
{entry.canDelete && (
<DropdownItem
onClick={deleteAudit}
variant="danger"