From 4013b00841ca144c1c1911f88ba9243e9b7e392c Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Fri, 2 Jan 2026 11:04:48 +0100 Subject: [PATCH] Refactor policies document Signed-off-by: Bryan Frimin --- .../iam/APIKeysPageQuery.graphql.ts | 13 +- ...ersonalAPIKeyListCreateMutation.graphql.ts | 17 +- .../iam/PersonalAPIKeyListFragment.graphql.ts | 36 +- ...alAPIKeyListRevealTokenMutation.graphql.ts | 92 --- ...ersonalAPIKeyListRevokeMutation.graphql.ts | 92 --- .../iam/PersonalAPIKeyRowFragment.graphql.ts | 101 +++ .../PersonalAPIKeyRowRefetchQuery.graphql.ts | 172 +++++ ...ersonalAPIKeyRow_revokeMutation.graphql.ts | 132 ++++ .../_components/PersonalAPIKeyList.tsx | 168 +---- .../apiKeys/_components/PersonalAPIKeyRow.tsx | 135 ++++ .../_components/PersonalAPIKeyTokenAction.tsx | 63 ++ .../_components/PersonalAPIKeyTokenDialog.tsx | 3 - .../_components/PersonalAPIKeysTable.tsx | 74 +-- pkg/coredata/asset.go | 15 + pkg/coredata/audit.go | 15 + pkg/coredata/connector.go | 20 +- pkg/coredata/continual_improvements.go | 20 +- pkg/coredata/control.go | 15 + pkg/coredata/custom_domain.go | 15 + .../data_protection_impact_assessment.go | 15 + pkg/coredata/datum.go | 20 +- pkg/coredata/document.go | 15 + pkg/coredata/document_version.go | 15 + pkg/coredata/document_version_signature.go | 15 + pkg/coredata/email.go | 6 + pkg/coredata/entity_type_reg.go | 105 +++ pkg/coredata/evidence.go | 15 + pkg/coredata/export_job.go | 39 +- pkg/coredata/file.go | 15 + pkg/coredata/framework.go | 15 + pkg/coredata/identity.go | 28 + pkg/coredata/invitation.go | 29 + pkg/coredata/measure.go | 15 + pkg/coredata/meeting.go | 15 + pkg/coredata/membership.go | 64 +- pkg/coredata/membership_profile.go | 14 + pkg/coredata/nonconformity.go | 25 +- pkg/coredata/obligation.go | 15 + pkg/coredata/organization.go | 14 + pkg/coredata/people.go | 14 + pkg/coredata/personal_api_key.go | 14 + pkg/coredata/processing_activities.go | 15 + pkg/coredata/report.go | 14 + pkg/coredata/risk.go | 14 + pkg/coredata/saml_configuration.go | 14 + pkg/coredata/session.go | 24 + pkg/coredata/slack_message.go | 40 +- pkg/coredata/snapshot.go | 15 + pkg/coredata/task.go | 14 + pkg/coredata/transfer_impact_assessment.go | 14 + pkg/coredata/trust_center.go | 14 + pkg/coredata/trust_center_access.go | 40 +- pkg/coredata/trust_center_document_access.go | 14 + pkg/coredata/trust_center_file.go | 15 + pkg/coredata/trust_center_reference.go | 14 + pkg/coredata/vendor.go | 14 + .../vendor_business_associate_agreement.go | 14 + pkg/coredata/vendor_compliance_report.go | 19 +- pkg/coredata/vendor_contact.go | 14 + pkg/coredata/vendor_data_privacy_agreement.go | 14 + pkg/coredata/vendor_risk_assessment.go | 15 + pkg/coredata/vendor_service.go | 14 + pkg/iam/authorizer.go | 187 ++++-- pkg/iam/iam_policies.go | 49 +- pkg/iam/policy/policy.go | 1 - pkg/iam/policy/statement.go | 34 +- pkg/iam/policy/statement_test.go | 19 +- pkg/iam/policy_set.go | 20 +- pkg/iam/policy_set_test.go | 10 +- pkg/iam/service.go | 25 + pkg/probo/policies.go | 49 +- pkg/server/api/connect/v1/schema.graphql | 21 +- pkg/server/api/connect/v1/schema/schema.go | 609 +++--------------- pkg/server/api/connect/v1/types/types.go | 31 +- pkg/server/api/connect/v1/v1_resolver.go | 48 +- 75 files changed, 1958 insertions(+), 1230 deletions(-) delete mode 100644 apps/console/src/__generated__/iam/PersonalAPIKeyListRevealTokenMutation.graphql.ts delete mode 100644 apps/console/src/__generated__/iam/PersonalAPIKeyListRevokeMutation.graphql.ts create mode 100644 apps/console/src/__generated__/iam/PersonalAPIKeyRowFragment.graphql.ts create mode 100644 apps/console/src/__generated__/iam/PersonalAPIKeyRowRefetchQuery.graphql.ts create mode 100644 apps/console/src/__generated__/iam/PersonalAPIKeyRow_revokeMutation.graphql.ts create mode 100644 apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyRow.tsx create mode 100644 apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyTokenAction.tsx diff --git a/apps/console/src/__generated__/iam/APIKeysPageQuery.graphql.ts b/apps/console/src/__generated__/iam/APIKeysPageQuery.graphql.ts index 769a320bf..00877c258 100644 --- a/apps/console/src/__generated__/iam/APIKeysPageQuery.graphql.ts +++ b/apps/console/src/__generated__/iam/APIKeysPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<83cc00d864e6388ef2156203499da849>> + * @generated SignedSource<<76057cc6f78c32b15c6e580c7a0c30da>> * @lightSyntaxTransform * @nogrep */ @@ -124,13 +124,6 @@ return { "name": "expiresAt", "storageKey": null }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "lastUsedAt", - "storageKey": null - }, { "alias": null, "args": null, @@ -194,12 +187,12 @@ return { ] }, "params": { - "cacheID": "14e698e7b6edf0afe9cf3398986cd529", + "cacheID": "cf6f6f621bdcb1bd24a2dd94f856729f", "id": null, "metadata": {}, "name": "APIKeysPageQuery", "operationKind": "query", - "text": "query APIKeysPageQuery {\n viewer {\n ...PersonalAPIKeyListFragment\n id\n }\n}\n\nfragment PersonalAPIKeyListFragment on Identity {\n id\n personalAPIKeys(first: 1000) {\n edges {\n node {\n id\n name\n createdAt\n expiresAt\n lastUsedAt\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n" + "text": "query APIKeysPageQuery {\n viewer {\n ...PersonalAPIKeyListFragment\n id\n }\n}\n\nfragment PersonalAPIKeyListFragment on Identity {\n id\n personalAPIKeys(first: 1000) {\n edges {\n node {\n id\n ...PersonalAPIKeyRowFragment\n __typename\n }\n cursor\n }\n pageInfo {\n endCursor\n hasNextPage\n }\n }\n}\n\nfragment PersonalAPIKeyRowFragment on PersonalAPIKey {\n id\n name\n createdAt\n expiresAt\n}\n" } }; })(); diff --git a/apps/console/src/__generated__/iam/PersonalAPIKeyListCreateMutation.graphql.ts b/apps/console/src/__generated__/iam/PersonalAPIKeyListCreateMutation.graphql.ts index f630499b7..e9c7782b5 100644 --- a/apps/console/src/__generated__/iam/PersonalAPIKeyListCreateMutation.graphql.ts +++ b/apps/console/src/__generated__/iam/PersonalAPIKeyListCreateMutation.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<875eae6f5455a17a1bb8e0590c7acdb0>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -12,7 +12,6 @@ import { ConcreteRequest } from 'relay-runtime'; export type CreatePersonalAPIKeyInput = { expiresAt: any; name: string; - organizationIds: ReadonlyArray; }; export type PersonalAPIKeyListCreateMutation$variables = { connections: ReadonlyArray; @@ -25,7 +24,6 @@ export type PersonalAPIKeyListCreateMutation$data = { readonly createdAt: any; readonly expiresAt: any; readonly id: string; - readonly lastUsedAt: any | null | undefined; readonly name: string; }; }; @@ -98,13 +96,6 @@ v3 = { "kind": "ScalarField", "name": "expiresAt", "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "lastUsedAt", - "storageKey": null } ], "storageKey": null @@ -187,16 +178,16 @@ return { ] }, "params": { - "cacheID": "5b1f6109730bd9f261afd6635d568b59", + "cacheID": "1ec09abe9f3a61e13ba2deacf1a273fb", "id": null, "metadata": {}, "name": "PersonalAPIKeyListCreateMutation", "operationKind": "mutation", - "text": "mutation PersonalAPIKeyListCreateMutation(\n $input: CreatePersonalAPIKeyInput!\n) {\n createPersonalAPIKey(input: $input) {\n personalAPIKeyEdge {\n node {\n id\n name\n createdAt\n expiresAt\n lastUsedAt\n }\n }\n token\n }\n}\n" + "text": "mutation PersonalAPIKeyListCreateMutation(\n $input: CreatePersonalAPIKeyInput!\n) {\n createPersonalAPIKey(input: $input) {\n personalAPIKeyEdge {\n node {\n id\n name\n createdAt\n expiresAt\n }\n }\n token\n }\n}\n" } }; })(); -(node as any).hash = "c7e932ad41ff2740e8687b7edb904431"; +(node as any).hash = "86df62e5e5f2d9f347f41648d7af9c32"; export default node; diff --git a/apps/console/src/__generated__/iam/PersonalAPIKeyListFragment.graphql.ts b/apps/console/src/__generated__/iam/PersonalAPIKeyListFragment.graphql.ts index 84a606d19..05c0c6eb0 100644 --- a/apps/console/src/__generated__/iam/PersonalAPIKeyListFragment.graphql.ts +++ b/apps/console/src/__generated__/iam/PersonalAPIKeyListFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -15,11 +15,8 @@ export type PersonalAPIKeyListFragment$data = { readonly personalAPIKeys: { readonly edges: ReadonlyArray<{ readonly node: { - readonly createdAt: any; - readonly expiresAt: any; readonly id: string; - readonly lastUsedAt: any | null | undefined; - readonly name: string; + readonly " $fragmentSpreads": FragmentRefs<"PersonalAPIKeyRowFragment">; }; }>; }; @@ -86,32 +83,9 @@ return { "selections": [ (v0/*: any*/), { - "alias": null, "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "createdAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "expiresAt", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "lastUsedAt", - "storageKey": null + "kind": "FragmentSpread", + "name": "PersonalAPIKeyRowFragment" }, { "alias": null, @@ -171,6 +145,6 @@ return { }; })(); -(node as any).hash = "70fa6b4e568fc1598b8dc23a030428b5"; +(node as any).hash = "eac59db5cdb5d77813236a90e7fb4d94"; export default node; diff --git a/apps/console/src/__generated__/iam/PersonalAPIKeyListRevealTokenMutation.graphql.ts b/apps/console/src/__generated__/iam/PersonalAPIKeyListRevealTokenMutation.graphql.ts deleted file mode 100644 index 99b0e019d..000000000 --- a/apps/console/src/__generated__/iam/PersonalAPIKeyListRevealTokenMutation.graphql.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @generated SignedSource<<7a6fb678e9147fd1810e02bd09d5cef1>> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -export type RevealPersonalAPIKeyTokenInput = { - tokenId: string; -}; -export type PersonalAPIKeyListRevealTokenMutation$variables = { - input: RevealPersonalAPIKeyTokenInput; -}; -export type PersonalAPIKeyListRevealTokenMutation$data = { - readonly revealPersonalAPIKeyToken: { - readonly token: string; - } | null | undefined; -}; -export type PersonalAPIKeyListRevealTokenMutation = { - response: PersonalAPIKeyListRevealTokenMutation$data; - variables: PersonalAPIKeyListRevealTokenMutation$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" - } -], -v1 = [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "RevealPersonalAPIKeyTokenPayload", - "kind": "LinkedField", - "name": "revealPersonalAPIKeyToken", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "token", - "storageKey": null - } - ], - "storageKey": null - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "PersonalAPIKeyListRevealTokenMutation", - "selections": (v1/*: any*/), - "type": "Mutation", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "PersonalAPIKeyListRevealTokenMutation", - "selections": (v1/*: any*/) - }, - "params": { - "cacheID": "25ffba7c8a69e22dcc46b56a6c8633ed", - "id": null, - "metadata": {}, - "name": "PersonalAPIKeyListRevealTokenMutation", - "operationKind": "mutation", - "text": "mutation PersonalAPIKeyListRevealTokenMutation(\n $input: RevealPersonalAPIKeyTokenInput!\n) {\n revealPersonalAPIKeyToken(input: $input) {\n token\n }\n}\n" - } -}; -})(); - -(node as any).hash = "7dad7ebd6e8086a01b2bac8aa80c532e"; - -export default node; diff --git a/apps/console/src/__generated__/iam/PersonalAPIKeyListRevokeMutation.graphql.ts b/apps/console/src/__generated__/iam/PersonalAPIKeyListRevokeMutation.graphql.ts deleted file mode 100644 index a92b868ee..000000000 --- a/apps/console/src/__generated__/iam/PersonalAPIKeyListRevokeMutation.graphql.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @generated SignedSource<<84eef5bca22c2ad2b73bbe068767e35a>> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -export type RevokePersonalAPIKeyInput = { - tokenId: string; -}; -export type PersonalAPIKeyListRevokeMutation$variables = { - input: RevokePersonalAPIKeyInput; -}; -export type PersonalAPIKeyListRevokeMutation$data = { - readonly revokePersonalAPIKey: { - readonly success: boolean; - } | null | undefined; -}; -export type PersonalAPIKeyListRevokeMutation = { - response: PersonalAPIKeyListRevokeMutation$data; - variables: PersonalAPIKeyListRevokeMutation$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "input" - } -], -v1 = [ - { - "alias": null, - "args": [ - { - "kind": "Variable", - "name": "input", - "variableName": "input" - } - ], - "concreteType": "RevokePersonalAPIKeyPayload", - "kind": "LinkedField", - "name": "revokePersonalAPIKey", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "success", - "storageKey": null - } - ], - "storageKey": null - } -]; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "PersonalAPIKeyListRevokeMutation", - "selections": (v1/*: any*/), - "type": "Mutation", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "PersonalAPIKeyListRevokeMutation", - "selections": (v1/*: any*/) - }, - "params": { - "cacheID": "fd05bb0236b583ca54be466cffe8f45b", - "id": null, - "metadata": {}, - "name": "PersonalAPIKeyListRevokeMutation", - "operationKind": "mutation", - "text": "mutation PersonalAPIKeyListRevokeMutation(\n $input: RevokePersonalAPIKeyInput!\n) {\n revokePersonalAPIKey(input: $input) {\n success\n }\n}\n" - } -}; -})(); - -(node as any).hash = "34ef04d19251c479827ca5028346f65b"; - -export default node; diff --git a/apps/console/src/__generated__/iam/PersonalAPIKeyRowFragment.graphql.ts b/apps/console/src/__generated__/iam/PersonalAPIKeyRowFragment.graphql.ts new file mode 100644 index 000000000..cb964f302 --- /dev/null +++ b/apps/console/src/__generated__/iam/PersonalAPIKeyRowFragment.graphql.ts @@ -0,0 +1,101 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type PersonalAPIKeyRowFragment$data = { + readonly createdAt: any; + readonly expiresAt: any; + readonly id: string; + readonly name: string; + readonly token?: string | null | undefined; + readonly " $fragmentType": "PersonalAPIKeyRowFragment"; +}; +export type PersonalAPIKeyRowFragment$key = { + readonly " $data"?: PersonalAPIKeyRowFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"PersonalAPIKeyRowFragment">; +}; + +import PersonalAPIKeyRowRefetchQuery_graphql from './PersonalAPIKeyRowRefetchQuery.graphql'; + +const node: ReaderFragment = { + "argumentDefinitions": [ + { + "defaultValue": false, + "kind": "LocalArgument", + "name": "includeToken" + } + ], + "kind": "Fragment", + "metadata": { + "refetch": { + "connection": null, + "fragmentPathInResult": [ + "node" + ], + "operation": PersonalAPIKeyRowRefetchQuery_graphql, + "identifierInfo": { + "identifierField": "id", + "identifierQueryVariableName": "id" + } + } + }, + "name": "PersonalAPIKeyRowFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, + { + "condition": "includeToken", + "kind": "Condition", + "passingValue": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "token", + "storageKey": null + } + ] + } + ], + "type": "PersonalAPIKey", + "abstractKey": null +}; + +(node as any).hash = "d17db443fa203ee5f9c7d0f4576295f0"; + +export default node; diff --git a/apps/console/src/__generated__/iam/PersonalAPIKeyRowRefetchQuery.graphql.ts b/apps/console/src/__generated__/iam/PersonalAPIKeyRowRefetchQuery.graphql.ts new file mode 100644 index 000000000..3ffad672e --- /dev/null +++ b/apps/console/src/__generated__/iam/PersonalAPIKeyRowRefetchQuery.graphql.ts @@ -0,0 +1,172 @@ +/** + * @generated SignedSource<<2d413d4e4cf2528a45ff243c0fa733bb>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type PersonalAPIKeyRowRefetchQuery$variables = { + id: string; + includeToken?: boolean | null | undefined; +}; +export type PersonalAPIKeyRowRefetchQuery$data = { + readonly node: { + readonly " $fragmentSpreads": FragmentRefs<"PersonalAPIKeyRowFragment">; + } | null | undefined; +}; +export type PersonalAPIKeyRowRefetchQuery = { + response: PersonalAPIKeyRowRefetchQuery$data; + variables: PersonalAPIKeyRowRefetchQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = { + "defaultValue": null, + "kind": "LocalArgument", + "name": "id" +}, +v1 = { + "defaultValue": false, + "kind": "LocalArgument", + "name": "includeToken" +}, +v2 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "id" + } +]; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "PersonalAPIKeyRowRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "args": [ + { + "kind": "Variable", + "name": "includeToken", + "variableName": "includeToken" + } + ], + "kind": "FragmentSpread", + "name": "PersonalAPIKeyRowFragment" + } + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "PersonalAPIKeyRowRefetchQuery", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "createdAt", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + }, + { + "condition": "includeToken", + "kind": "Condition", + "passingValue": true, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "token", + "storageKey": null + } + ] + } + ], + "type": "PersonalAPIKey", + "abstractKey": null + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "b4732244e28d5847baaa5520a95a4cfb", + "id": null, + "metadata": {}, + "name": "PersonalAPIKeyRowRefetchQuery", + "operationKind": "query", + "text": "query PersonalAPIKeyRowRefetchQuery(\n $includeToken: Boolean = false\n $id: ID!\n) {\n node(id: $id) {\n __typename\n ...PersonalAPIKeyRowFragment_2T7Twf\n id\n }\n}\n\nfragment PersonalAPIKeyRowFragment_2T7Twf on PersonalAPIKey {\n id\n name\n createdAt\n expiresAt\n token @include(if: $includeToken)\n}\n" + } +}; +})(); + +(node as any).hash = "d17db443fa203ee5f9c7d0f4576295f0"; + +export default node; diff --git a/apps/console/src/__generated__/iam/PersonalAPIKeyRow_revokeMutation.graphql.ts b/apps/console/src/__generated__/iam/PersonalAPIKeyRow_revokeMutation.graphql.ts new file mode 100644 index 000000000..9d9b3cbcf --- /dev/null +++ b/apps/console/src/__generated__/iam/PersonalAPIKeyRow_revokeMutation.graphql.ts @@ -0,0 +1,132 @@ +/** + * @generated SignedSource<<64ad5ad6ff333dffc7b2d5d578028d7f>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +export type RevokePersonalAPIKeyInput = { + personalAPIKeyId: string; +}; +export type PersonalAPIKeyRow_revokeMutation$variables = { + connections: ReadonlyArray; + input: RevokePersonalAPIKeyInput; +}; +export type PersonalAPIKeyRow_revokeMutation$data = { + readonly revokePersonalAPIKey: { + readonly personalAPIKeyId: string; + } | null | undefined; +}; +export type PersonalAPIKeyRow_revokeMutation = { + response: PersonalAPIKeyRow_revokeMutation$data; + variables: PersonalAPIKeyRow_revokeMutation$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, + "kind": "ScalarField", + "name": "personalAPIKeyId", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [ + (v0/*: any*/), + (v1/*: any*/) + ], + "kind": "Fragment", + "metadata": null, + "name": "PersonalAPIKeyRow_revokeMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "RevokePersonalAPIKeyPayload", + "kind": "LinkedField", + "name": "revokePersonalAPIKey", + "plural": false, + "selections": [ + (v3/*: any*/) + ], + "storageKey": null + } + ], + "type": "Mutation", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [ + (v1/*: any*/), + (v0/*: any*/) + ], + "kind": "Operation", + "name": "PersonalAPIKeyRow_revokeMutation", + "selections": [ + { + "alias": null, + "args": (v2/*: any*/), + "concreteType": "RevokePersonalAPIKeyPayload", + "kind": "LinkedField", + "name": "revokePersonalAPIKey", + "plural": false, + "selections": [ + (v3/*: any*/), + { + "alias": null, + "args": null, + "filters": null, + "handle": "deleteEdge", + "key": "", + "kind": "ScalarHandle", + "name": "personalAPIKeyId", + "handleArgs": [ + { + "kind": "Variable", + "name": "connections", + "variableName": "connections" + } + ] + } + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "29dfd9de3537585482c2863a1d547031", + "id": null, + "metadata": {}, + "name": "PersonalAPIKeyRow_revokeMutation", + "operationKind": "mutation", + "text": "mutation PersonalAPIKeyRow_revokeMutation(\n $input: RevokePersonalAPIKeyInput!\n) {\n revokePersonalAPIKey(input: $input) {\n personalAPIKeyId\n }\n}\n" + } +}; +})(); + +(node as any).hash = "258ca02064f55b6fd4a3630ba1f904a4"; + +export default node; diff --git a/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyList.tsx b/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyList.tsx index 512b08590..a7d8e871c 100644 --- a/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyList.tsx +++ b/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyList.tsx @@ -6,10 +6,9 @@ import { useFragment, useMutation, } from "react-relay"; -import type { RecordSourceSelectorProxy } from "relay-runtime"; import { z } from "zod"; import { useTranslate } from "@probo/i18n"; -import { formatError, type GraphQLError } from "@probo/helpers"; +import { formatError } from "@probo/helpers"; import { Breadcrumb, Button, @@ -22,15 +21,12 @@ import { Label, Option, Select, - useConfirm, useDialogRef, useToast, } from "@probo/ui"; import { useFormWithSchema } from "/hooks/useFormWithSchema"; import type { PersonalAPIKeyListFragment$key } from "/__generated__/iam/PersonalAPIKeyListFragment.graphql"; import type { PersonalAPIKeyListCreateMutation } from "/__generated__/iam/PersonalAPIKeyListCreateMutation.graphql"; -import type { PersonalAPIKeyListRevokeMutation } from "/__generated__/iam/PersonalAPIKeyListRevokeMutation.graphql"; -import type { PersonalAPIKeyListRevealTokenMutation } from "/__generated__/iam/PersonalAPIKeyListRevealTokenMutation.graphql"; import { PersonalAPIKeysTable } from "./PersonalAPIKeysTable"; import { PersonalAPIKeyTokenDialog } from "./PersonalAPIKeyTokenDialog"; @@ -44,10 +40,7 @@ const fragment = graphql` edges @required(action: THROW) { node { id - name - createdAt - expiresAt - lastUsedAt + ...PersonalAPIKeyRowFragment } } } @@ -66,7 +59,6 @@ const createMutation = graphql` name createdAt expiresAt - lastUsedAt } } token @@ -74,26 +66,6 @@ const createMutation = graphql` } `; -const revokeMutation = graphql` - mutation PersonalAPIKeyListRevokeMutation( - $input: RevokePersonalAPIKeyInput! - ) { - revokePersonalAPIKey(input: $input) { - success - } - } -`; - -const revealTokenMutation = graphql` - mutation PersonalAPIKeyListRevealTokenMutation( - $input: RevealPersonalAPIKeyTokenInput! - ) { - revealPersonalAPIKeyToken(input: $input) { - token - } - } -`; - const createSchema = z.object({ name: z.string().min(1, "Name is required"), expiresIn: z.enum(["1month", "3months", "6months", "1year"]), @@ -126,7 +98,6 @@ export function PersonalAPIKeyList(props: { const { fKey } = props; const { __ } = useTranslate(); const { toast } = useToast(); - const confirm = useConfirm(); const createDialogRef = useDialogRef(); const tokenDialogRef = useDialogRef(); @@ -134,9 +105,12 @@ export function PersonalAPIKeyList(props: { const viewer = useFragment(fragment, fKey); - const keys = viewer.personalAPIKeys.edges.map(({ node }) => node); + const connectionID = ConnectionHandler.getConnectionID( + viewer.id, + "PersonalAPIKeyListFragment_personalAPIKeys" + ); - const { formState, handleSubmit, register, control, reset, watch } = + const { formState, handleSubmit, register, control, reset } = useFormWithSchema(createSchema, { defaultValues: { name: new Date().toISOString().split("T")[0], @@ -144,20 +118,14 @@ export function PersonalAPIKeyList(props: { }, }); - watch(); - const [createCommit, isCreating] = useMutation(createMutation); - const [revokeCommit] = - useMutation(revokeMutation); - const [revealTokenCommit, isRevealingToken] = - useMutation(revealTokenMutation); const handleCreate = (data: CreateFormData) => { const expiresAt = computeExpiresAt(data.expiresIn); const connectionID = ConnectionHandler.getConnectionID( viewer.id, - "PersonalAPIKeyListFragment_personalAPIKeys", + "PersonalAPIKeyListFragment_personalAPIKeys" ); createCommit({ @@ -165,8 +133,6 @@ export function PersonalAPIKeyList(props: { input: { name: data.name, expiresAt: expiresAt.toISOString(), - // API keys are no longer linked to organizations; keep schema compatibility. - organizationIds: [], }, connections: [connectionID], }, @@ -194,116 +160,6 @@ export function PersonalAPIKeyList(props: { }); }; - const handleRevoke = (key: { id: string; name: string }) => { - confirm( - async () => { - await new Promise((resolve, reject) => { - revokeCommit({ - variables: { - input: { tokenId: key.id }, - }, - updater: (store: RecordSourceSelectorProxy) => { - const viewerRecord = store.getRoot().getLinkedRecord("viewer"); - if (!viewerRecord) return; - const connection = ConnectionHandler.getConnection( - viewerRecord, - "PersonalAPIKeyListFragment_personalAPIKeys", - ); - if (connection) { - ConnectionHandler.deleteNode(connection, key.id); - } - }, - onCompleted: (_response, errors) => { - if (errors?.length) { - toast({ - title: __("Error"), - description: formatError( - __("Failed to revoke API key."), - errors as GraphQLError[], - ), - variant: "error", - }); - reject(errors); - return; - } - toast({ - title: __("Success"), - description: __("API key revoked successfully."), - variant: "success", - }); - resolve(); - }, - onError: (error) => { - toast({ - title: __("Error"), - description: formatError( - __("Failed to revoke API key."), - error, - ), - variant: "error", - }); - reject(error); - }, - }); - }); - }, - { - title: __("Revoke API Key"), - message: __( - `Are you sure you want to revoke the API key "${key.name}"? This action cannot be undone.`, - ), - label: __("Revoke"), - variant: "danger", - }, - ); - }; - - const handleShowToken = (key: { id: string; name: string }) => { - revealTokenCommit({ - variables: { - input: { - tokenId: key.id, - }, - }, - onCompleted: (response, errors) => { - if (errors?.length) { - toast({ - title: __("Error"), - description: formatError( - __("Failed to reveal API key token."), - errors as any, - ), - variant: "error", - }); - return; - } - - const tokenValue = response.revealPersonalAPIKeyToken?.token; - if (!tokenValue) { - toast({ - title: __("Error"), - description: __("No token returned."), - variant: "error", - }); - return; - } - - setToken(tokenValue); - tokenDialogRef.current?.open(); - }, - onError: (error: Error) => { - toast({ - title: __("Error"), - description: formatError( - __("Failed to reveal API key token."), - error, - ), - variant: "error", - }); - }, - }); - }; - return ( <>
@@ -314,7 +170,7 @@ export function PersonalAPIKeyList(props: {
- {keys.length === 0 ? ( + {viewer.personalAPIKeys.edges.length === 0 ? (

@@ -328,10 +184,8 @@ export function PersonalAPIKeyList(props: { ) : ( )} diff --git a/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyRow.tsx b/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyRow.tsx new file mode 100644 index 000000000..12235c537 --- /dev/null +++ b/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyRow.tsx @@ -0,0 +1,135 @@ +import { useTranslate } from "@probo/i18n"; +import { formatDate, formatError, type GraphQLError } from "@probo/helpers"; +import { Button, Spinner, Td, Tr, useConfirm, useToast } from "@probo/ui"; +import { graphql } from "relay-runtime"; +import { useMutation, useFragment } from "react-relay"; +import type { PersonalAPIKeyRowFragment$key } from "/__generated__/iam/PersonalAPIKeyRowFragment.graphql"; +import type { PersonalAPIKeyRow_revokeMutation } from "/__generated__/iam/PersonalAPIKeyRow_revokeMutation.graphql"; +import clsx from "clsx"; +import { PersonalAPIKeyTokenAction } from "./PersonalAPIKeyTokenAction"; +import { Suspense } from "react"; + +const revokeMutation = graphql` + mutation PersonalAPIKeyRow_revokeMutation( + $input: RevokePersonalAPIKeyInput! + $connections: [ID!]! + ) { + revokePersonalAPIKey(input: $input) { + personalAPIKeyId @deleteEdge(connections: $connections) + } + } +`; + +export const personalAPIKeyRowFragment = graphql` + fragment PersonalAPIKeyRowFragment on PersonalAPIKey + @refetchable(queryName: "PersonalAPIKeyRowRefetchQuery") + @argumentDefinitions(includeToken: { type: "Boolean", defaultValue: false }) { + id + name + createdAt + expiresAt + token @include(if: $includeToken) + } +`; + +export function PersonalAPIKeyRow(props: { + fKey: PersonalAPIKeyRowFragment$key; + connectionId: string; +}) { + const { fKey, connectionId } = props; + const { __ } = useTranslate(); + const confirm = useConfirm(); + const { toast } = useToast(); + const now = new Date(); + + const key = useFragment(personalAPIKeyRowFragment, fKey); + const expired = new Date(key.expiresAt) < now; + + const [revokeCommit, isRevoking] = + useMutation(revokeMutation); + + const handleRevoke = () => { + confirm( + async () => { + await new Promise((resolve, reject) => { + revokeCommit({ + variables: { + input: { personalAPIKeyId: key.id }, + connections: [connectionId], + }, + onCompleted: (_response, errors) => { + if (errors?.length) { + toast({ + title: __("Error"), + description: formatError( + __("Failed to revoke API key."), + errors as GraphQLError[] + ), + variant: "error", + }); + reject(errors); + return; + } + toast({ + title: __("Success"), + description: __("API key revoked successfully."), + variant: "success", + }); + resolve(); + }, + onError: (error) => { + toast({ + title: __("Error"), + description: formatError( + __("Failed to revoke API key."), + error + ), + variant: "error", + }); + reject(error); + }, + }); + }); + }, + { + title: __("Revoke API Key"), + message: __( + `Are you sure you want to revoke the API key "${key.name}"? This action cannot be undone.` + ), + label: __("Revoke"), + variant: "danger", + } + ); + }; + + return ( + + +
{key.name}
+
+ {expired ? __("Expired") : __("Active")} +
+ + + + {formatDate(key.createdAt)} + + + + + {formatDate(key.expiresAt)} + + + +
+ }> + + + +
+ + + ); +} diff --git a/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyTokenAction.tsx b/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyTokenAction.tsx new file mode 100644 index 000000000..2cab48d64 --- /dev/null +++ b/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyTokenAction.tsx @@ -0,0 +1,63 @@ +import { useTranslate } from "@probo/i18n"; +import { Button, useDialogRef, useToast } from "@probo/ui"; +import { formatError } from "@probo/helpers"; +import { useRefetchableFragment } from "react-relay"; +import { PersonalAPIKeyTokenDialog } from "./PersonalAPIKeyTokenDialog"; +import { personalAPIKeyRowFragment } from "./PersonalAPIKeyRow"; +import type { PersonalAPIKeyRowFragment$key } from "/__generated__/iam/PersonalAPIKeyRowFragment.graphql"; +import type { PersonalAPIKeyRowRefetchQuery } from "/__generated__/iam/PersonalAPIKeyRowRefetchQuery.graphql"; + +export function PersonalAPIKeyTokenAction(props: { + fKey: PersonalAPIKeyRowFragment$key; + disabled?: boolean; +}) { + const { fKey, disabled } = props; + const { __ } = useTranslate(); + const { toast } = useToast(); + const dialogRef = useDialogRef(); + + const [data, refetch] = useRefetchableFragment< + PersonalAPIKeyRowRefetchQuery, + PersonalAPIKeyRowFragment$key + >(personalAPIKeyRowFragment, fKey); + + const handleShow = () => { + dialogRef.current?.open(); + + refetch( + { includeToken: true }, + { + fetchPolicy: "network-only", + onComplete: (error) => { + if (error) { + toast({ + title: __("Error"), + description: formatError( + __("Failed to load API key token."), + error + ), + variant: "error", + }); + dialogRef.current?.close(); + } + }, + } + ); + }; + + return ( + <> + + + { + dialogRef.current?.close(); + }} + /> + + ); +} diff --git a/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyTokenDialog.tsx b/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyTokenDialog.tsx index bda4c7ad5..f9ffec72d 100644 --- a/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyTokenDialog.tsx +++ b/apps/console/src/pages/iam/apiKeys/_components/PersonalAPIKeyTokenDialog.tsx @@ -7,7 +7,6 @@ import { } from "@probo/ui"; import { useTranslate } from "@probo/i18n"; import { useCopy } from "@probo/hooks"; -import { useEffect } from "react"; export function PersonalAPIKeyTokenDialog(props: { dialogRef: React.RefObject<{ open: () => void; close: () => void } | null>; @@ -18,8 +17,6 @@ export function PersonalAPIKeyTokenDialog(props: { const { __ } = useTranslate(); const [isCopied, copy] = useCopy(); - useEffect(() => {}, [token]); - return ( void; - onShowToken: (key: { id: string; name: string }) => void; - isShowingToken?: boolean; + edges: PersonalAPIKeyListFragment$data["personalAPIKeys"]["edges"]; + connectionId: string; }) { - const { keys, onRevoke, onShowToken, isShowingToken } = props; + const { edges, connectionId } = props; const { __ } = useTranslate(); - const now = new Date(); return ( @@ -32,51 +22,13 @@ export function PersonalAPIKeysTable(props: { - {keys.map((k) => { - const expired = new Date(k.expiresAt) < now; - return ( - - - - - - - - ); - })} + {edges.map(({ node }) => ( + + ))}
-
{k.name}
-
- {expired ? __("Expired") : __("Active")} -
-
- - {k.lastUsedAt ? formatDate(k.lastUsedAt) : "—"} - - - - {formatDate(k.createdAt)} - - - - {formatDate(k.expiresAt)} - - -
- - -
-
); diff --git a/pkg/coredata/asset.go b/pkg/coredata/asset.go index 597186336..c09f25452 100644 --- a/pkg/coredata/asset.go +++ b/pkg/coredata/asset.go @@ -56,6 +56,21 @@ func (a *Asset) CursorKey(field AssetOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", field)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (a *Asset) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM assets WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, a.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query asset authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (a *Asset) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/audit.go b/pkg/coredata/audit.go index ab0dece30..a0d1fc8d0 100644 --- a/pkg/coredata/audit.go +++ b/pkg/coredata/audit.go @@ -60,6 +60,21 @@ func (a *Audit) CursorKey(field AuditOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", field)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (a *Audit) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM audits WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, a.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query audit authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (a *Audit) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/connector.go b/pkg/coredata/connector.go index b03d17ad6..3a7784deb 100644 --- a/pkg/coredata/connector.go +++ b/pkg/coredata/connector.go @@ -17,16 +17,17 @@ package coredata import ( "context" "encoding/json" + "errors" "fmt" "maps" "time" + "github.com/jackc/pgx/v5" + "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/connector" "go.probo.inc/probo/pkg/crypto/cipher" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/page" - "github.com/jackc/pgx/v5" - "go.gearno.de/kit/pg" ) type ( @@ -56,6 +57,21 @@ func (c *Connector) CursorKey(orderBy ConnectorOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (c *Connector) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM connectors WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, c.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query connector authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (c *Connectors) LoadByOrganizationID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/continual_improvements.go b/pkg/coredata/continual_improvements.go index 9837364cc..c0fe6f4b9 100644 --- a/pkg/coredata/continual_improvements.go +++ b/pkg/coredata/continual_improvements.go @@ -16,14 +16,15 @@ package coredata import ( "context" + "errors" "fmt" "maps" "time" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/page" "github.com/jackc/pgx/v5" "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/page" ) type ( @@ -63,6 +64,21 @@ func (ci *ContinualImprovement) CursorKey(field ContinualImprovementOrderField) panic(fmt.Sprintf("unsupported order by: %s", field)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (ci *ContinualImprovement) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM continual_improvements WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, ci.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query continual improvement authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (ci *ContinualImprovement) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/control.go b/pkg/coredata/control.go index 509d93a1b..b878889c6 100644 --- a/pkg/coredata/control.go +++ b/pkg/coredata/control.go @@ -57,6 +57,21 @@ func (c Control) CursorKey(orderBy ControlOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (c *Control) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM controls WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, c.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query control authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (c *Controls) CountByDocumentID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/custom_domain.go b/pkg/coredata/custom_domain.go index 4e05c6c82..6ec924d8c 100644 --- a/pkg/coredata/custom_domain.go +++ b/pkg/coredata/custom_domain.go @@ -65,6 +65,21 @@ func NewCustomDomain(tenantID gid.TenantID, domain string) *CustomDomain { } } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (cd *CustomDomain) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM custom_domains WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, cd.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query custom domain authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (cd *CustomDomain) CursorKey(field CustomDomainOrderField) page.CursorKey { switch field { case CustomDomainOrderFieldCreatedAt: diff --git a/pkg/coredata/data_protection_impact_assessment.go b/pkg/coredata/data_protection_impact_assessment.go index e46532430..7be9b71fa 100644 --- a/pkg/coredata/data_protection_impact_assessment.go +++ b/pkg/coredata/data_protection_impact_assessment.go @@ -69,6 +69,21 @@ func (dpia *DataProtectionImpactAssessment) CursorKey(field DataProtectionImpact panic(fmt.Sprintf("unsupported order by: %s", field)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (dpia *DataProtectionImpactAssessment) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM processing_activity_data_protection_impact_assessments WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, dpia.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query data protection impact assessment authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (dpias *DataProtectionImpactAssessments) CountByOrganizationID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/datum.go b/pkg/coredata/datum.go index fbc069f58..3f73fcd03 100644 --- a/pkg/coredata/datum.go +++ b/pkg/coredata/datum.go @@ -16,14 +16,15 @@ package coredata import ( "context" + "errors" "fmt" "maps" "time" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/page" "github.com/jackc/pgx/v5" "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/page" ) type ( @@ -59,6 +60,21 @@ func (d *Datum) CursorKey(field DatumOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", field)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (d *Datum) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM data WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, d.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query datum authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (d *Datum) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/document.go b/pkg/coredata/document.go index 87171ea81..e8a523794 100644 --- a/pkg/coredata/document.go +++ b/pkg/coredata/document.go @@ -58,6 +58,21 @@ func (p Document) CursorKey(orderBy DocumentOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (d *Document) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM documents WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, d.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query document authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (p *Document) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/document_version.go b/pkg/coredata/document_version.go index 6ea24c114..ff5f6f960 100644 --- a/pkg/coredata/document_version.go +++ b/pkg/coredata/document_version.go @@ -48,6 +48,21 @@ type ( DocumentVersions []*DocumentVersion ) +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (dv *DocumentVersion) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM document_versions WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, dv.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query document version authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (p *DocumentVersions) LoadByDocumentID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/document_version_signature.go b/pkg/coredata/document_version_signature.go index a9d2d4a97..3762935f6 100644 --- a/pkg/coredata/document_version_signature.go +++ b/pkg/coredata/document_version_signature.go @@ -62,6 +62,21 @@ func (pvs DocumentVersionSignature) CursorKey(orderBy DocumentVersionSignatureOr panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (dvs *DocumentVersionSignature) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM document_version_signatures WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, dvs.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query document version signature authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (pvs *DocumentVersionSignature) LoadByDocumentVersionIDAndSignatory( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/email.go b/pkg/coredata/email.go index 0426cec07..822d1cf04 100644 --- a/pkg/coredata/email.go +++ b/pkg/coredata/email.go @@ -44,6 +44,12 @@ var ( ErrNoUnsentEmail = errors.New("no unsent email found") ) +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +// Email is identity-scoped (not org-scoped), so it returns an empty map. +func (e *Email) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + return map[string]string{}, nil +} + func NewEmail( recipientName string, recipientEmail mail.Addr, diff --git a/pkg/coredata/entity_type_reg.go b/pkg/coredata/entity_type_reg.go index 659ffbd9d..0794bba7e 100644 --- a/pkg/coredata/entity_type_reg.go +++ b/pkg/coredata/entity_type_reg.go @@ -14,6 +14,8 @@ package coredata +import "go.probo.inc/probo/pkg/gid" + type ctxKey struct{ name string } var ( @@ -75,6 +77,109 @@ const ( MembershipProfileEntityType uint16 = 51 ) +func NewEntityFromID(id gid.GID) (any, bool) { + switch id.EntityType() { + case OrganizationEntityType: + return &Organization{ID: id}, true + case FrameworkEntityType: + return &Framework{ID: id}, true + case MeasureEntityType: + return &Measure{ID: id}, true + case TaskEntityType: + return &Task{ID: id}, true + case EvidenceEntityType: + return &Evidence{ID: id}, true + case ConnectorEntityType: + return &Connector{ID: id}, true + case VendorRiskAssessmentEntityType: + return &VendorRiskAssessment{ID: id}, true + case VendorEntityType: + return &Vendor{ID: id}, true + case PeopleEntityType: + return &People{ID: id}, true + case VendorComplianceReportEntityType: + return &VendorComplianceReport{ID: id}, true + case DocumentEntityType: + return &Document{ID: id}, true + case IdentityEntityType: + return &Identity{ID: id}, true + case SessionEntityType: + return &Session{ID: id}, true + case EmailEntityType: + return &Email{ID: id}, true + case ControlEntityType: + return &Control{ID: id}, true + case RiskEntityType: + return &Risk{ID: id}, true + case DocumentVersionEntityType: + return &DocumentVersion{ID: id}, true + case DocumentVersionSignatureEntityType: + return &DocumentVersionSignature{ID: id}, true + case AssetEntityType: + return &Asset{ID: id}, true + case DatumEntityType: + return &Datum{ID: id}, true + case AuditEntityType: + return &Audit{ID: id}, true + case ReportEntityType: + return &Report{ID: id}, true + case TrustCenterEntityType: + return &TrustCenter{ID: id}, true + case TrustCenterAccessEntityType: + return &TrustCenterAccess{ID: id}, true + case VendorBusinessAssociateAgreementEntityType: + return &VendorBusinessAssociateAgreement{ID: id}, true + case FileEntityType: + return &File{ID: id}, true + case VendorContactEntityType: + return &VendorContact{ID: id}, true + case VendorDataPrivacyAgreementEntityType: + return &VendorDataPrivacyAgreement{ID: id}, true + case NonconformityEntityType: + return &Nonconformity{ID: id}, true + case ObligationEntityType: + return &Obligation{ID: id}, true + case VendorServiceEntityType: + return &VendorService{ID: id}, true + case SnapshotEntityType: + return &Snapshot{ID: id}, true + case ContinualImprovementEntityType: + return &ContinualImprovement{ID: id}, true + case ProcessingActivityEntityType: + return &ProcessingActivity{ID: id}, true + case ExportJobEntityType: + return &ExportJob{ID: id}, true + case TrustCenterReferenceEntityType: + return &TrustCenterReference{ID: id}, true + case TrustCenterDocumentAccessEntityType: + return &TrustCenterDocumentAccess{ID: id}, true + case CustomDomainEntityType: + return &CustomDomain{ID: id}, true + case InvitationEntityType: + return &Invitation{ID: id}, true + case MembershipEntityType: + return &Membership{ID: id}, true + case SlackMessageEntityType: + return &SlackMessage{ID: id}, true + case TrustCenterFileEntityType: + return &TrustCenterFile{ID: id}, true + case SAMLConfigurationEntityType: + return &SAMLConfiguration{ID: id}, true + case PersonalAPIKeyEntityType: + return &PersonalAPIKey{ID: id}, true + case MeetingEntityType: + return &Meeting{ID: id}, true + case DataProtectionImpactAssessmentEntityType: + return &DataProtectionImpactAssessment{ID: id}, true + case TransferImpactAssessmentEntityType: + return &TransferImpactAssessment{ID: id}, true + case MembershipProfileEntityType: + return &MembershipProfile{ID: id}, true + default: + return nil, false + } +} + type EntityInfo struct { Model string Table string diff --git a/pkg/coredata/evidence.go b/pkg/coredata/evidence.go index 9670b6f33..fcb36867b 100644 --- a/pkg/coredata/evidence.go +++ b/pkg/coredata/evidence.go @@ -56,6 +56,21 @@ func (e Evidence) CursorKey(orderBy EvidenceOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (e *Evidence) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM evidences WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, e.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query evidence authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (e Evidence) Upsert( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/export_job.go b/pkg/coredata/export_job.go index 0df5235b2..1d80b95a3 100644 --- a/pkg/coredata/export_job.go +++ b/pkg/coredata/export_job.go @@ -16,18 +16,18 @@ import ( type ( ExportJob struct { - ID gid.GID `db:"id"` - OrganizationID gid.GID `db:"organization_id"` - Type ExportJobType `db:"type"` - Arguments json.RawMessage `db:"arguments"` - Error *string `db:"error"` - Status ExportJobStatus `db:"status"` - FileID *gid.GID `db:"file_id"` - RecipientEmail mail.Addr `db:"recipient_email"` - RecipientName string `db:"recipient_name"` - CreatedAt time.Time `db:"created_at"` - StartedAt *time.Time `db:"started_at"` - CompletedAt *time.Time `db:"completed_at"` + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + Type ExportJobType `db:"type"` + Arguments json.RawMessage `db:"arguments"` + Error *string `db:"error"` + Status ExportJobStatus `db:"status"` + FileID *gid.GID `db:"file_id"` + RecipientEmail mail.Addr `db:"recipient_email"` + RecipientName string `db:"recipient_name"` + CreatedAt time.Time `db:"created_at"` + StartedAt *time.Time `db:"started_at"` + CompletedAt *time.Time `db:"completed_at"` } ExportJobs []*ExportJob @@ -48,6 +48,21 @@ var ( ErrNoExportJobAvailable = errors.New("no export job available") ) +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (ej *ExportJob) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM export_jobs WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, ej.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query export job authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (ej *ExportJob) Insert( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/file.go b/pkg/coredata/file.go index 2e4da523a..9605582c9 100644 --- a/pkg/coredata/file.go +++ b/pkg/coredata/file.go @@ -44,6 +44,21 @@ type ( Files []*File ) +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (f *File) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM files WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, f.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query file authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (f *File) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/framework.go b/pkg/coredata/framework.go index d8dc31a5a..ce6993fd9 100644 --- a/pkg/coredata/framework.go +++ b/pkg/coredata/framework.go @@ -53,6 +53,21 @@ func (f *Framework) CursorKey(orderBy FrameworkOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (f *Framework) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM frameworks WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, f.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query framework authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (f *Frameworks) CountByOrganizationID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/identity.go b/pkg/coredata/identity.go index 9bba52a3c..c22fde585 100644 --- a/pkg/coredata/identity.go +++ b/pkg/coredata/identity.go @@ -220,6 +220,34 @@ LIMIT 1; return nil } +// AuthorizationAttributes loads the minimal authorization attributes for policy condition evaluation. +// It is intentionally lightweight and does not populate the Identity struct. +func (i *Identity) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := ` +SELECT + id, + email_address +FROM + identities +WHERE + id = $1 +LIMIT 1; +` + + var ( + id gid.GID + emailAddress string + ) + if err := conn.QueryRow(ctx, q, i.ID).Scan(&id, &emailAddress); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query identity iam attributes: %w", err) + } + + return map[string]string{"id": id.String(), "email": emailAddress}, nil +} + func (i *Identity) Insert( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/invitation.go b/pkg/coredata/invitation.go index 7a0ac3dc5..3c4f3e8e8 100644 --- a/pkg/coredata/invitation.go +++ b/pkg/coredata/invitation.go @@ -164,6 +164,35 @@ WHERE return nil } +// AuthorizationAttributes loads the minimal authorization attributes for policy condition evaluation. +// It is intentionally lightweight and does not populate the Invitation struct. +func (i *Invitation) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := ` +SELECT + email + , organization_id +FROM + iam_invitations +WHERE + id = $1 +LIMIT 1; +` + + var email string + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, i.ID).Scan(&email, &organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query invitation iam attributes: %w", err) + } + + return map[string]string{ + "email": email, + "organization_id": organizationID.String(), + }, nil +} + func (i *Invitation) Update(ctx context.Context, conn pg.Conn, scope Scoper) error { query := ` UPDATE diff --git a/pkg/coredata/measure.go b/pkg/coredata/measure.go index f804f69ae..1b97e5533 100644 --- a/pkg/coredata/measure.go +++ b/pkg/coredata/measure.go @@ -56,6 +56,21 @@ func (m Measure) CursorKey(orderBy MeasureOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (m *Measure) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM measures WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, m.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query measure authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (m *Measures) CountByRiskID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/meeting.go b/pkg/coredata/meeting.go index 4a7aea234..8246a82a8 100644 --- a/pkg/coredata/meeting.go +++ b/pkg/coredata/meeting.go @@ -54,6 +54,21 @@ func (m Meeting) CursorKey(orderBy MeetingOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +// AuthorizationAttributes returns the authorization attributes for policy evaluation. +func (m *Meeting) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM meetings WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, m.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query meeting authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (m *Meeting) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/membership.go b/pkg/coredata/membership.go index d6af16239..d4ccecb0e 100644 --- a/pkg/coredata/membership.go +++ b/pkg/coredata/membership.go @@ -226,6 +226,33 @@ LEFT JOIN return nil } +func (m *Membership) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := ` +SELECT + identity_id + , organization_id +FROM + iam_memberships +WHERE + id = $1 +LIMIT 1; +` + + var identityID gid.GID + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, m.ID).Scan(&identityID, &organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query membership iam attributes: %w", err) + } + + return map[string]string{ + "identity_id": identityID.String(), + "organization_id": organizationID.String(), + }, nil +} + func LoadRoleByIdentityAndEntityIDOnly( ctx context.Context, conn pg.Conn, @@ -235,7 +262,6 @@ func LoadRoleByIdentityAndEntityIDOnly( ) (MembershipRole, error) { entityType := entityID.EntityType() - // For organization, the entity ID is the organization ID - optimized path if entityType == OrganizationEntityType { query := ` SELECT role @@ -595,3 +621,39 @@ WHERE return count, nil } + +func (m *Memberships) LoadAllByIdentityID( + ctx context.Context, + conn pg.Conn, + identityID gid.GID, +) error { + q := ` +SELECT + id, + identity_id, + organization_id, + role, + '' as full_name, + NULL as email_address, + created_at, + updated_at +FROM + iam_memberships +WHERE + identity_id = $1 +; +` + + rows, err := conn.Query(ctx, q, identityID) + if err != nil { + return fmt.Errorf("cannot query memberships: %w", err) + } + + memberships, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Membership]) + if err != nil { + return fmt.Errorf("cannot collect memberships: %w", err) + } + + *m = memberships + return nil +} diff --git a/pkg/coredata/membership_profile.go b/pkg/coredata/membership_profile.go index bec17a983..2a0a5c271 100644 --- a/pkg/coredata/membership_profile.go +++ b/pkg/coredata/membership_profile.go @@ -36,6 +36,20 @@ type ( } ) +func (p *MembershipProfile) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT m.organization_id FROM iam_membership_profiles mp JOIN iam_memberships m ON mp.membership_id = m.id WHERE mp.id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, p.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query membership profile authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (p *MembershipProfile) LoadByMembershipID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/nonconformity.go b/pkg/coredata/nonconformity.go index f6e02acdf..df535399d 100644 --- a/pkg/coredata/nonconformity.go +++ b/pkg/coredata/nonconformity.go @@ -16,14 +16,15 @@ package coredata import ( "context" + "errors" "fmt" "maps" "time" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/page" "github.com/jackc/pgx/v5" "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/page" ) type ( @@ -66,6 +67,20 @@ func (nc *Nonconformity) CursorKey(field NonconformityOrderField) page.CursorKey panic(fmt.Sprintf("unsupported order by: %s", field)) } +func (nc *Nonconformity) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM nonconformities WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, nc.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query nonconformity authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (nc *Nonconformity) LoadByID( ctx context.Context, conn pg.Conn, @@ -399,9 +414,9 @@ WHERE %s AND nc.organization_id = @organization_id AND nc.snapshot_id IS NULL query = fmt.Sprintf(query, scope.SQLFragment()) args := pgx.StrictNamedArgs{ - "tenant_id": scope.GetTenantID(), - "snapshot_id": snapshotID, - "organization_id": organizationID, + "tenant_id": scope.GetTenantID(), + "snapshot_id": snapshotID, + "organization_id": organizationID, "nonconformity_entity_type": NonconformityEntityType, } maps.Copy(args, scope.SQLArguments()) diff --git a/pkg/coredata/obligation.go b/pkg/coredata/obligation.go index 2084d93f1..153ebc6b5 100644 --- a/pkg/coredata/obligation.go +++ b/pkg/coredata/obligation.go @@ -16,6 +16,7 @@ package coredata import ( "context" + "errors" "fmt" "maps" "time" @@ -64,6 +65,20 @@ func (o *Obligation) CursorKey(field ObligationOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", field)) } +func (o *Obligation) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM obligations WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, o.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query obligation authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (o *Obligation) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/organization.go b/pkg/coredata/organization.go index fdf4e3fca..707f44647 100644 --- a/pkg/coredata/organization.go +++ b/pkg/coredata/organization.go @@ -46,6 +46,20 @@ type ( Organizations []*Organization ) +func (o *Organization) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT id FROM organizations WHERE id = $1 LIMIT 1;` + + var id gid.GID + if err := conn.QueryRow(ctx, q, o.ID).Scan(&id); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query organization authorization attributes: %w", err) + } + + return map[string]string{"organization_id": o.ID.String()}, nil +} + func (o Organization) CursorKey(orderBy OrganizationOrderField) page.CursorKey { switch orderBy { case OrganizationOrderFieldName: diff --git a/pkg/coredata/people.go b/pkg/coredata/people.go index e124220f5..3186a5b0a 100644 --- a/pkg/coredata/people.go +++ b/pkg/coredata/people.go @@ -60,6 +60,20 @@ func (p People) CursorKey(orderBy PeopleOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (p *People) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM peoples WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, p.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query people authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (p *People) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/personal_api_key.go b/pkg/coredata/personal_api_key.go index 5f49a2deb..a97c0fe42 100644 --- a/pkg/coredata/personal_api_key.go +++ b/pkg/coredata/personal_api_key.go @@ -91,6 +91,20 @@ LIMIT 1; return nil } +func (a *PersonalAPIKey) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := "SELECT identity_id FROM iam_personal_api_keys WHERE id = $1 LIMIT 1;" + + var identityID gid.GID + if err := conn.QueryRow(ctx, q, a.ID).Scan(&identityID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query personal api key iam attributes: %w", err) + } + + return map[string]string{"identity_id": identityID.String()}, nil +} + func (a *PersonalAPIKeys) LoadByIdentityID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/processing_activities.go b/pkg/coredata/processing_activities.go index 64ef6f0a2..50e9172fb 100644 --- a/pkg/coredata/processing_activities.go +++ b/pkg/coredata/processing_activities.go @@ -16,6 +16,7 @@ package coredata import ( "context" + "errors" "fmt" "maps" "time" @@ -75,6 +76,20 @@ func (p *ProcessingActivity) CursorKey(field ProcessingActivityOrderField) page. panic(fmt.Sprintf("unsupported order by: %s", field)) } +func (p *ProcessingActivity) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM processing_activities WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, p.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query processing activity authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (p *ProcessingActivity) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/report.go b/pkg/coredata/report.go index 1048dae8b..467a69748 100644 --- a/pkg/coredata/report.go +++ b/pkg/coredata/report.go @@ -42,6 +42,20 @@ type ( Reports []*Report ) +func (r *Report) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM reports WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, r.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query report authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (r *Report) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/risk.go b/pkg/coredata/risk.go index 1eb1eb810..66a32d0dd 100644 --- a/pkg/coredata/risk.go +++ b/pkg/coredata/risk.go @@ -80,6 +80,20 @@ func (r *Risk) CursorKey(orderBy RiskOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (r *Risk) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM risks WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, r.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query risk authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (r *Risks) CountByMeasureID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/saml_configuration.go b/pkg/coredata/saml_configuration.go index 568aecce0..d1c0dbcf4 100644 --- a/pkg/coredata/saml_configuration.go +++ b/pkg/coredata/saml_configuration.go @@ -63,6 +63,20 @@ func (s *SAMLConfiguration) CursorKey(orderBy SAMLConfigurationOrderField) page. panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (s *SAMLConfiguration) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM iam_saml_configurations WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, s.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query saml configuration authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (s *SAMLConfiguration) GetIdPCertificate() (*x509.Certificate, error) { block, _ := pem.Decode([]byte(s.IdPCertificate)) if block == nil { diff --git a/pkg/coredata/session.go b/pkg/coredata/session.go index f80dd217c..cdbebbc7c 100644 --- a/pkg/coredata/session.go +++ b/pkg/coredata/session.go @@ -140,6 +140,30 @@ LIMIT 1; return nil } +// AuthorizationAttributes loads the minimal authorization attributes for policy condition evaluation. +// It is intentionally lightweight and does not populate the Session struct. +func (s *Session) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := ` +SELECT + identity_id +FROM + iam_sessions +WHERE + id = $1 +LIMIT 1; +` + + var identityID gid.GID + if err := conn.QueryRow(ctx, q, s.ID).Scan(&identityID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query session iam attributes: %w", err) + } + + return map[string]string{"identity_id": identityID.String()}, nil +} + func (s *Session) Insert( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/slack_message.go b/pkg/coredata/slack_message.go index 4df02d71a..3ca00436a 100644 --- a/pkg/coredata/slack_message.go +++ b/pkg/coredata/slack_message.go @@ -29,19 +29,19 @@ import ( type ( SlackMessage struct { - ID gid.GID `db:"id"` - OrganizationID gid.GID `db:"organization_id"` - Type SlackMessageType `db:"type"` - Body map[string]any `db:"body"` - MessageTS *string `db:"message_ts"` - ChannelID *string `db:"channel_id"` - RequesterEmail *mail.Addr `db:"requester_email"` - Metadata map[string]any `db:"metadata"` - InitialSlackMessageID gid.GID `db:"initial_slack_message_id"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - SentAt *time.Time `db:"sent_at"` - Error *string `db:"error"` + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + Type SlackMessageType `db:"type"` + Body map[string]any `db:"body"` + MessageTS *string `db:"message_ts"` + ChannelID *string `db:"channel_id"` + RequesterEmail *mail.Addr `db:"requester_email"` + Metadata map[string]any `db:"metadata"` + InitialSlackMessageID gid.GID `db:"initial_slack_message_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + SentAt *time.Time `db:"sent_at"` + Error *string `db:"error"` } ErrNoUnsentSlackMessage struct{} @@ -57,6 +57,20 @@ func (e ErrSlackMessageNotFound) Error() string { return "slack message not found" } +func (sm *SlackMessage) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM slack_messages WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, sm.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query slack message authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func NewSlackMessage( scope Scoper, organizationID gid.GID, diff --git a/pkg/coredata/snapshot.go b/pkg/coredata/snapshot.go index 37bf88b9e..344beb3fa 100644 --- a/pkg/coredata/snapshot.go +++ b/pkg/coredata/snapshot.go @@ -16,6 +16,7 @@ package coredata import ( "context" + "errors" "fmt" "maps" "time" @@ -52,6 +53,20 @@ func (s *Snapshot) CursorKey(field SnapshotOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", field)) } +func (s *Snapshot) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM snapshots WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, s.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query snapshot authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (s *Snapshot) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/task.go b/pkg/coredata/task.go index 6de67b756..a8aa3ef4b 100644 --- a/pkg/coredata/task.go +++ b/pkg/coredata/task.go @@ -57,6 +57,20 @@ func (c Task) CursorKey(orderBy TaskOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (t *Task) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM tasks WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, t.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query task authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (c *Task) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/transfer_impact_assessment.go b/pkg/coredata/transfer_impact_assessment.go index f60aa6bb2..3afc3e069 100644 --- a/pkg/coredata/transfer_impact_assessment.go +++ b/pkg/coredata/transfer_impact_assessment.go @@ -69,6 +69,20 @@ func (tia *TransferImpactAssessment) CursorKey(field TransferImpactAssessmentOrd panic(fmt.Sprintf("unsupported order by: %s", field)) } +func (tia *TransferImpactAssessment) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM processing_activity_transfer_impact_assessments WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, tia.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query transfer impact assessment authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (tias *TransferImpactAssessments) CountByOrganizationID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/trust_center.go b/pkg/coredata/trust_center.go index 87c0bab3e..810663a3c 100644 --- a/pkg/coredata/trust_center.go +++ b/pkg/coredata/trust_center.go @@ -52,6 +52,20 @@ func (tc *TrustCenter) CursorKey(orderBy TrustCenterOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (tc *TrustCenter) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM trust_centers WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, tc.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query trust center authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (tc *TrustCenter) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/trust_center_access.go b/pkg/coredata/trust_center_access.go index b1b88b66b..5111c00fb 100644 --- a/pkg/coredata/trust_center_access.go +++ b/pkg/coredata/trust_center_access.go @@ -32,19 +32,19 @@ import ( type ( TrustCenterAccess struct { - ID gid.GID `db:"id"` - OrganizationID gid.GID `db:"organization_id"` - TenantID gid.TenantID `db:"tenant_id"` - TrustCenterID gid.GID `db:"trust_center_id"` - Email mail.Addr `db:"email"` - Name string `db:"name"` - Active bool `db:"active"` - HasAcceptedNonDisclosureAgreement bool `db:"has_accepted_non_disclosure_agreement"` - HasAcceptedNonDisclosureAgreementMetadata json.RawMessage `db:"has_accepted_non_disclosure_agreement_metadata"` - NDAFileID *gid.GID `db:"nda_file_id"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` - LastTokenExpiresAt *time.Time `db:"last_token_expires_at"` + ID gid.GID `db:"id"` + OrganizationID gid.GID `db:"organization_id"` + TenantID gid.TenantID `db:"tenant_id"` + TrustCenterID gid.GID `db:"trust_center_id"` + Email mail.Addr `db:"email"` + Name string `db:"name"` + Active bool `db:"active"` + HasAcceptedNonDisclosureAgreement bool `db:"has_accepted_non_disclosure_agreement"` + HasAcceptedNonDisclosureAgreementMetadata json.RawMessage `db:"has_accepted_non_disclosure_agreement_metadata"` + NDAFileID *gid.GID `db:"nda_file_id"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + LastTokenExpiresAt *time.Time `db:"last_token_expires_at"` } TrustCenterAccesses []*TrustCenterAccess @@ -59,6 +59,20 @@ func (tca *TrustCenterAccess) CursorKey(orderBy TrustCenterAccessOrderField) pag panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (tca *TrustCenterAccess) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM trust_center_accesses WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, tca.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query trust center access authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (tca *TrustCenterAccess) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/trust_center_document_access.go b/pkg/coredata/trust_center_document_access.go index f970fc37b..2555f7bda 100644 --- a/pkg/coredata/trust_center_document_access.go +++ b/pkg/coredata/trust_center_document_access.go @@ -53,6 +53,20 @@ func (tcda *TrustCenterDocumentAccess) CursorKey(orderBy TrustCenterDocumentAcce panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (tcda *TrustCenterDocumentAccess) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM trust_center_document_accesses WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, tcda.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query trust center document access authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (tcda *TrustCenterDocumentAccess) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/trust_center_file.go b/pkg/coredata/trust_center_file.go index 42cf176bd..35b90bf43 100644 --- a/pkg/coredata/trust_center_file.go +++ b/pkg/coredata/trust_center_file.go @@ -16,6 +16,7 @@ package coredata import ( "context" + "errors" "fmt" "maps" "time" @@ -53,6 +54,20 @@ func (t TrustCenterFile) CursorKey(orderBy TrustCenterFileOrderField) page.Curso panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (t *TrustCenterFile) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM trust_center_files WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, t.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query trust center file authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (t *TrustCenterFile) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/trust_center_reference.go b/pkg/coredata/trust_center_reference.go index f2f1c5838..0e80423c9 100644 --- a/pkg/coredata/trust_center_reference.go +++ b/pkg/coredata/trust_center_reference.go @@ -59,6 +59,20 @@ func (t TrustCenterReference) CursorKey(orderBy TrustCenterReferenceOrderField) panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (t *TrustCenterReference) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM trust_center_references WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, t.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query trust center reference authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (t *TrustCenterReference) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/vendor.go b/pkg/coredata/vendor.go index 56cd56b76..cde5cc255 100644 --- a/pkg/coredata/vendor.go +++ b/pkg/coredata/vendor.go @@ -78,6 +78,20 @@ func (v Vendor) CursorKey(orderBy VendorOrderField) page.CursorKey { panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (v *Vendor) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM vendors WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, v.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query vendor authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (v *Vendor) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/vendor_business_associate_agreement.go b/pkg/coredata/vendor_business_associate_agreement.go index 5ba4d3e34..5c1164617 100644 --- a/pkg/coredata/vendor_business_associate_agreement.go +++ b/pkg/coredata/vendor_business_associate_agreement.go @@ -56,6 +56,20 @@ func (v VendorBusinessAssociateAgreement) CursorKey(orderBy VendorBusinessAssoci panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (vbaa *VendorBusinessAssociateAgreement) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM vendor_business_associate_agreements WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, vbaa.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query vendor business associate agreement authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (vbaa *VendorBusinessAssociateAgreement) LoadByVendorID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/vendor_compliance_report.go b/pkg/coredata/vendor_compliance_report.go index a2a242c1e..af388c1cb 100644 --- a/pkg/coredata/vendor_compliance_report.go +++ b/pkg/coredata/vendor_compliance_report.go @@ -16,14 +16,15 @@ package coredata import ( "context" + "errors" "fmt" "maps" "time" - "go.probo.inc/probo/pkg/gid" - "go.probo.inc/probo/pkg/page" "github.com/jackc/pgx/v5" "go.gearno.de/kit/pg" + "go.probo.inc/probo/pkg/gid" + "go.probo.inc/probo/pkg/page" ) type ( @@ -55,6 +56,20 @@ func (c VendorComplianceReport) CursorKey(orderBy VendorComplianceReportOrderFie panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (v *VendorComplianceReport) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM vendor_compliance_reports WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, v.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query vendor compliance report authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (vcs *VendorComplianceReports) LoadForVendorID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/vendor_contact.go b/pkg/coredata/vendor_contact.go index c719a5674..6ded780e8 100644 --- a/pkg/coredata/vendor_contact.go +++ b/pkg/coredata/vendor_contact.go @@ -59,6 +59,20 @@ func (vc VendorContact) CursorKey(orderBy VendorContactOrderField) page.CursorKe panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (vc *VendorContact) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM vendor_contacts WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, vc.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query vendor contact authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (vc *VendorContact) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/vendor_data_privacy_agreement.go b/pkg/coredata/vendor_data_privacy_agreement.go index ee3356ee9..628a72cd8 100644 --- a/pkg/coredata/vendor_data_privacy_agreement.go +++ b/pkg/coredata/vendor_data_privacy_agreement.go @@ -56,6 +56,20 @@ func (v VendorDataPrivacyAgreement) CursorKey(orderBy VendorDataPrivacyAgreement panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (vdpa *VendorDataPrivacyAgreement) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM vendor_data_privacy_agreements WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, vdpa.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query vendor data privacy agreement authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (vdpa *VendorDataPrivacyAgreement) LoadByVendorID( ctx context.Context, conn pg.Conn, diff --git a/pkg/coredata/vendor_risk_assessment.go b/pkg/coredata/vendor_risk_assessment.go index 47604a944..263f03bf2 100644 --- a/pkg/coredata/vendor_risk_assessment.go +++ b/pkg/coredata/vendor_risk_assessment.go @@ -16,6 +16,7 @@ package coredata import ( "context" + "errors" "fmt" "maps" "time" @@ -56,6 +57,20 @@ func (v VendorRiskAssessment) CursorKey(orderBy VendorRiskAssessmentOrderField) panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (v *VendorRiskAssessment) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM vendor_risk_assessments WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, v.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query vendor risk assessment authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + // Insert adds a new risk assessment to the database func (r VendorRiskAssessment) Insert( ctx context.Context, diff --git a/pkg/coredata/vendor_service.go b/pkg/coredata/vendor_service.go index 4dcc0866a..008ef40a0 100644 --- a/pkg/coredata/vendor_service.go +++ b/pkg/coredata/vendor_service.go @@ -54,6 +54,20 @@ func (vs VendorService) CursorKey(orderBy VendorServiceOrderField) page.CursorKe panic(fmt.Sprintf("unsupported order by: %s", orderBy)) } +func (vs *VendorService) AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) { + q := `SELECT organization_id FROM vendor_services WHERE id = $1 LIMIT 1;` + + var organizationID gid.GID + if err := conn.QueryRow(ctx, q, vs.ID).Scan(&organizationID); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, ErrResourceNotFound + } + return nil, fmt.Errorf("cannot query vendor service authorization attributes: %w", err) + } + + return map[string]string{"organization_id": organizationID.String()}, nil +} + func (vs *VendorService) LoadByID( ctx context.Context, conn pg.Conn, diff --git a/pkg/iam/authorizer.go b/pkg/iam/authorizer.go index c39154d36..f27a66a71 100644 --- a/pkg/iam/authorizer.go +++ b/pkg/iam/authorizer.go @@ -16,7 +16,7 @@ package iam import ( "context" - "errors" + "fmt" "maps" "go.gearno.de/kit/pg" @@ -25,12 +25,28 @@ import ( "go.probo.inc/probo/pkg/iam/policy" ) +// AuthorizationAttributer is implemented by entities that provide attributes +// for policy condition evaluation. +type AuthorizationAttributer interface { + AuthorizationAttributes(ctx context.Context, conn pg.Conn) (map[string]string, error) +} + +// AuthorizeParams contains the parameters for an authorization request. +type AuthorizeParams struct { + Principal gid.GID + Resource gid.GID + Action string + ResourceAttributes map[string]string +} + +// Authorizer evaluates authorization requests against registered policies. type Authorizer struct { pg *pg.Client evaluator *policy.Evaluator policySet *PolicySet } +// NewAuthorizer creates a new Authorizer instance. func NewAuthorizer(pgClient *pg.Client) *Authorizer { return &Authorizer{ pg: pgClient, @@ -39,87 +55,144 @@ func NewAuthorizer(pgClient *pg.Client) *Authorizer { } } -func (a *Authorizer) RegisterPolicySet(policySet *PolicySet) { - a.policySet.Merge(policySet) -} - -type AuthorizeParams struct { - Principal gid.GID - Resource gid.GID - Action string - ResourceAttributes map[string]string +// RegisterPolicySet merges the given policy set into the authorizer. +func (a *Authorizer) RegisterPolicySet(ps *PolicySet) { + a.policySet.Merge(ps) } +// Authorize checks if the principal is allowed to perform the action on the resource. func (a *Authorizer) Authorize(ctx context.Context, params AuthorizeParams) error { if params.Principal.EntityType() != coredata.IdentityEntityType { return NewUnsupportedPrincipalTypeError(params.Principal.EntityType()) } - policies := a.buildPolicies(ctx, params) + return a.pg.WithConn(ctx, func(conn pg.Conn) error { return a.authorize(ctx, conn, params) }) +} - // Pre-allocate Resource map with capacity for id + attributes - resourceAttrs := make(map[string]string, 1+len(params.ResourceAttributes)) - resourceAttrs["id"] = params.Resource.String() - maps.Copy(resourceAttrs, params.ResourceAttributes) - - conditionCtx := policy.ConditionContext{ - Principal: map[string]string{ - "id": params.Principal.String(), - }, - Resource: resourceAttrs, +func (a *Authorizer) authorize(ctx context.Context, conn pg.Conn, params AuthorizeParams) error { + memberships, err := a.loadMemberships(ctx, conn, params.Principal) + if err != nil { + return err } + resourceAttrs, err := a.buildResourceAttributes(ctx, conn, params) + if err != nil { + return err + } + + // Find role for resource's organization + resourceOrgID := resourceAttrs["organization_id"] + role := findRoleForOrg(memberships, resourceOrgID) + + // Only set principal.organization_id if they have a role in this org + var principalOrgID string + if role != "" { + principalOrgID = resourceOrgID + } + + principalAttrs, err := a.buildPrincipalAttributes(ctx, conn, params.Principal, principalOrgID) + if err != nil { + return err + } + + policies := a.buildPoliciesForRole(role) + req := policy.AuthorizationRequest{ - Principal: params.Principal, - Resource: params.Resource, - Action: params.Action, - ConditionContext: conditionCtx, + Principal: params.Principal, + Resource: params.Resource, + Action: params.Action, + ConditionContext: policy.ConditionContext{ + Principal: principalAttrs, + Resource: resourceAttrs, + }, } - result := a.evaluator.Evaluate(req, policies) - - if result.IsAllowed() { + if a.evaluator.Evaluate(req, policies).IsAllowed() { return nil } return NewInsufficientPermissionsError(params.Principal, params.Resource, params.Action) } -func (a *Authorizer) buildPolicies(ctx context.Context, params AuthorizeParams) []*policy.Policy { - selfManageCount := len(a.policySet.SelfManagePolicies) +func (a *Authorizer) loadMemberships(ctx context.Context, conn pg.Conn, principalID gid.GID) (coredata.Memberships, error) { + var memberships coredata.Memberships + if err := memberships.LoadAllByIdentityID(ctx, conn, principalID); err != nil { + return nil, fmt.Errorf("cannot load memberships: %w", err) + } + return memberships, nil +} - var rolePolicies []*policy.Policy - if params.Resource.TenantID() != gid.NilTenant { - rolePolicies = a.loadRolePolicies(ctx, params.Principal, params.Resource) +func (a *Authorizer) buildPrincipalAttributes( + ctx context.Context, + conn pg.Conn, + principalID gid.GID, + organizationID string, +) (map[string]string, error) { + attrs := map[string]string{ + "id": principalID.String(), + "organization_id": organizationID, } - totalCount := selfManageCount + len(rolePolicies) - policies := make([]*policy.Policy, selfManageCount, totalCount) - copy(policies, a.policySet.SelfManagePolicies) - policies = append(policies, rolePolicies...) + if entity, ok := coredata.NewEntityFromID(principalID); ok { + if attributer, ok := entity.(AuthorizationAttributer); ok { + entityAttrs, err := attributer.AuthorizationAttributes(ctx, conn) + if err != nil { + return nil, fmt.Errorf("cannot load principal attributes: %w", err) + } + maps.Copy(attrs, entityAttrs) + } + } + + return attrs, nil +} + +func (a *Authorizer) buildResourceAttributes( + ctx context.Context, + conn pg.Conn, + params AuthorizeParams, +) (map[string]string, error) { + attrs := map[string]string{ + "id": params.Resource.String(), + } + + entity, ok := coredata.NewEntityFromID(params.Resource) + if !ok { + return nil, fmt.Errorf("unsupported resource type: %d", params.Resource.EntityType()) + } + + attributer, ok := entity.(AuthorizationAttributer) + if !ok { + return nil, fmt.Errorf("resource %d does not implement AuthorizationAttributer", params.Resource.EntityType()) + } + + entityAttrs, err := attributer.AuthorizationAttributes(ctx, conn) + if err != nil { + return nil, fmt.Errorf("cannot load resource attributes: %w", err) + } + maps.Copy(attrs, entityAttrs) + + if params.ResourceAttributes != nil { + maps.Copy(attrs, params.ResourceAttributes) + } + + return attrs, nil +} + +func (a *Authorizer) buildPoliciesForRole(role string) []*policy.Policy { + policies := append([]*policy.Policy{}, a.policySet.IdentityScopedPolicies...) + + if role != "" { + policies = append(policies, a.policySet.RolePolicies[role]...) + } return policies } -func (a *Authorizer) loadRolePolicies(ctx context.Context, principalID gid.GID, resourceID gid.GID) []*policy.Policy { - var role coredata.MembershipRole - - err := a.pg.WithConn( - ctx, - func(conn pg.Conn) (err error) { - scope := coredata.NewScopeFromObjectID(resourceID) - role, err = coredata.LoadRoleByIdentityAndEntityIDOnly(ctx, conn, scope, principalID, resourceID) - if errors.Is(err, coredata.ErrResourceNotFound) { - return nil // No membership = no role-based policies - } - - return err - }, - ) - - if err != nil || role == "" { - return nil +func findRoleForOrg(memberships coredata.Memberships, orgID string) string { + for _, m := range memberships { + if m.OrganizationID.String() == orgID { + return string(m.Role) + } } - - return a.policySet.RolePolicies[role.String()] + return "" } diff --git a/pkg/iam/iam_policies.go b/pkg/iam/iam_policies.go index e5019e20f..66483837a 100644 --- a/pkg/iam/iam_policies.go +++ b/pkg/iam/iam_policies.go @@ -54,7 +54,7 @@ var IAMSelfManageSessionPolicy = policy.NewPolicy( ActionIAMSessionRevoke, ActionIAMSessionRevokeAll, ).WithSID("manage-own-sessions"). - When(policy.Equals("principal.id", "resource.user_id")), + When(policy.Equals("principal.id", "resource.identity_id")), ).WithDescription("Allows users to view and revoke their own sessions") // IAMSelfManageInvitationPolicy allows users to manage invitations sent to them. @@ -66,7 +66,7 @@ var IAMSelfManageInvitationPolicy = policy.NewPolicy( ActionIAMInvitationGet, ActionIAMInvitationAccept, ).WithSID("manage-own-invitations"). - When(policy.Equals("principal.id", "resource.user_id")), + When(policy.Equals("principal.email", "resource.email")), ).WithDescription("Allows users to view and accept invitations sent to them") // IAMSelfManageMembershipPolicy allows users to view their own memberships. @@ -77,7 +77,7 @@ var IAMSelfManageMembershipPolicy = policy.NewPolicy( policy.Allow( ActionIAMMembershipGet, ).WithSID("view-own-memberships"). - When(policy.Equals("principal.id", "resource.user_id")), + When(policy.Equals("principal.id", "resource.identity_id")), ).WithDescription("Allows users to view their organization memberships") // IAMSelfManagePersonalAPIKeyPolicy allows users to manage their own API keys. @@ -91,7 +91,7 @@ var IAMSelfManagePersonalAPIKeyPolicy = policy.NewPolicy( ActionIAMPersonalAPIKeyUpdate, ActionIAMPersonalAPIKeyDelete, ).WithSID("manage-own-api-keys"). - When(policy.Equals("principal.id", "resource.user_id")), + When(policy.Equals("principal.id", "resource.identity_id")), ).WithDescription("Allows users to manage their own personal API keys") // IAMOwnerPolicy defines permissions for organization owners. @@ -99,17 +99,20 @@ var IAMOwnerPolicy = policy.NewPolicy( "iam:owner", "Organization Owner", // Full access to organization management - policy.Allow("iam:organization:*").WithSID("full-org-access"), - // Full access to member management - policy.Allow("iam:membership:*").WithSID("full-membership-access"), - // Can manage invitations + policy.Allow("iam:organization:*").WithSID("full-org-access").When(policy.Equals("principal.organization_id", "resource.id")), + // Full access to member management (scoped to own organization) + policy.Allow("iam:membership:*").WithSID("full-membership-access"). + When(policy.Equals("principal.organization_id", "resource.organization_id")), + // Can manage invitations (scoped to own organization) policy.Allow( ActionIAMInvitationCreate, ActionIAMInvitationGet, ActionIAMInvitationDelete, - ).WithSID("manage-invitations"), - // Full access to SAML configuration management - policy.Allow("iam:saml-configuration:*").WithSID("full-saml-access"), + ).WithSID("manage-invitations"). + When(policy.Equals("principal.organization_id", "resource.organization_id")), + // Full access to SAML configuration management (scoped to own organization) + policy.Allow("iam:saml-configuration:*").WithSID("full-saml-access"). + When(policy.Equals("principal.organization_id", "resource.organization_id")), ).WithDescription("Full IAM access for organization owners") // IAMAdminPolicy defines permissions for organization admins. @@ -123,19 +126,22 @@ var IAMAdminPolicy = policy.NewPolicy( ActionIAMOrganizationListMembers, ActionIAMOrganizationListInvitations, ActionIAMOrganizationInviteMember, - ).WithSID("org-admin-access"), - // Can manage memberships (but not remove owner) + ).WithSID("org-admin-access").When(policy.Equals("principal.organization_id", "resource.organization_id")), + // Can manage memberships (scoped to own organization) policy.Allow( ActionIAMMembershipGet, ActionIAMMembershipUpdate, - ).WithSID("membership-admin-access"), - // Can manage invitations + ).WithSID("membership-admin-access"). + When(policy.Equals("principal.organization_id", "resource.organization_id")), + // Can manage invitations (scoped to own organization) policy.Allow( ActionIAMInvitationGet, ActionIAMInvitationDelete, - ).WithSID("invitation-admin-access"), - // Can view and update SAML configurations - policy.Allow(ActionIAMSAMLConfigurationGet).WithSID("saml-configuration-admin-access"), + ).WithSID("invitation-admin-access"). + When(policy.Equals("principal.organization_id", "resource.organization_id")), + // Can view SAML configurations (scoped to own organization) + policy.Allow(ActionIAMSAMLConfigurationGet).WithSID("saml-configuration-admin-access"). + When(policy.Equals("principal.organization_id", "resource.organization_id")), // Cannot delete organization policy.Deny(ActionIAMOrganizationDelete).WithSID("deny-org-delete"), // Cannot remove members (only owner can) @@ -156,7 +162,8 @@ var IAMViewerPolicy = policy.NewPolicy( policy.Allow( ActionIAMOrganizationGet, ActionIAMOrganizationListMembers, - ).WithSID("org-viewer-access"), - // Can view memberships - policy.Allow(ActionIAMMembershipGet).WithSID("membership-viewer-access"), + ).WithSID("org-viewer-access").When(policy.Equals("principal.organization_id", "resource.id")), + // Can view memberships (scoped to own organization) + policy.Allow(ActionIAMMembershipGet).WithSID("membership-viewer-access"). + When(policy.Equals("principal.organization_id", "resource.organization_id")), ).WithDescription("Read-only IAM access for organization viewers") diff --git a/pkg/iam/policy/policy.go b/pkg/iam/policy/policy.go index 432b07b38..ecbd82794 100644 --- a/pkg/iam/policy/policy.go +++ b/pkg/iam/policy/policy.go @@ -131,4 +131,3 @@ func ForEntityType(entityType uint16) ResourcePattern { EntityType: &entityType, } } - diff --git a/pkg/iam/policy/statement.go b/pkg/iam/policy/statement.go index 37c602b46..33cfe970a 100644 --- a/pkg/iam/policy/statement.go +++ b/pkg/iam/policy/statement.go @@ -15,6 +15,8 @@ package policy import ( + "strings" + "go.probo.inc/probo/pkg/gid" ) @@ -139,7 +141,22 @@ func (c Condition) Evaluate(ctx ConditionContext) bool { case ConditionIn: for _, v := range c.Values { resolved, ok := resolveValue(v, ctx) - if ok && value == resolved { + if !ok { + continue + } + + // Support a comma-separated "set" value, e.g. + // principal.organization_ids = "org_1,org_2" + if strings.Contains(resolved, ",") { + for _, item := range strings.Split(resolved, ",") { + if value == strings.TrimSpace(item) { + return true + } + } + continue + } + + if value == resolved { return true } } @@ -148,7 +165,20 @@ func (c Condition) Evaluate(ctx ConditionContext) bool { case ConditionNotIn: for _, v := range c.Values { resolved, ok := resolveValue(v, ctx) - if ok && value == resolved { + if !ok { + continue + } + + if strings.Contains(resolved, ",") { + for _, item := range strings.Split(resolved, ",") { + if value == strings.TrimSpace(item) { + return false + } + } + continue + } + + if value == resolved { return false } } diff --git a/pkg/iam/policy/statement_test.go b/pkg/iam/policy/statement_test.go index 67d1d3099..a0f3fac7d 100644 --- a/pkg/iam/policy/statement_test.go +++ b/pkg/iam/policy/statement_test.go @@ -88,15 +88,28 @@ func TestCondition_Evaluate_Equals(t *testing.T) { want: false, }, { - name: "equals - match resource.user_id reference", + name: "equals - match resource.identity_id reference", condition: Condition{ Operator: ConditionEquals, Key: "principal.id", - Values: []string{"resource.user_id"}, + Values: []string{"resource.identity_id"}, }, ctx: ConditionContext{ Principal: map[string]string{"id": "user_123"}, - Resource: map[string]string{"user_id": "user_123"}, + Resource: map[string]string{"identity_id": "user_123"}, + }, + want: true, + }, + { + name: "equals - match principal.email to resource.email reference", + condition: Condition{ + Operator: ConditionEquals, + Key: "principal.email", + Values: []string{"resource.email"}, + }, + ctx: ConditionContext{ + Principal: map[string]string{"email": "user@example.com"}, + Resource: map[string]string{"email": "user@example.com"}, }, want: true, }, diff --git a/pkg/iam/policy_set.go b/pkg/iam/policy_set.go index e7ac2e96f..6b476dc51 100644 --- a/pkg/iam/policy_set.go +++ b/pkg/iam/policy_set.go @@ -16,21 +16,21 @@ package iam import "go.probo.inc/probo/pkg/iam/policy" -// PolicySet holds role-based and self-management policies. +// PolicySet holds organization-scoped (role) policies and identity-scoped policies. // Services create their own PolicySet and combine them when creating the Authorizer. type PolicySet struct { // RolePolicies maps role names to policies. RolePolicies map[string][]*policy.Policy - // SelfManagePolicies are applied to all authenticated users. - SelfManagePolicies []*policy.Policy + // IdentityScopedPolicies are applied to all authenticated users, independent of organization membership. + IdentityScopedPolicies []*policy.Policy } // NewPolicySet creates an empty PolicySet. func NewPolicySet() *PolicySet { return &PolicySet{ - RolePolicies: make(map[string][]*policy.Policy), - SelfManagePolicies: make([]*policy.Policy, 0), + RolePolicies: make(map[string][]*policy.Policy), + IdentityScopedPolicies: make([]*policy.Policy, 0), } } @@ -40,9 +40,9 @@ func (ps *PolicySet) AddRolePolicy(role string, policies ...*policy.Policy) *Pol return ps } -// AddSelfManagePolicy adds policies applied to all authenticated users. -func (ps *PolicySet) AddSelfManagePolicy(policies ...*policy.Policy) *PolicySet { - ps.SelfManagePolicies = append(ps.SelfManagePolicies, policies...) +// AddIdentityScopedPolicy adds policies applied to all authenticated users (identity-scoped). +func (ps *PolicySet) AddIdentityScopedPolicy(policies ...*policy.Policy) *PolicySet { + ps.IdentityScopedPolicies = append(ps.IdentityScopedPolicies, policies...) return ps } @@ -51,7 +51,7 @@ func (ps *PolicySet) Merge(other *PolicySet) *PolicySet { for role, policies := range other.RolePolicies { ps.RolePolicies[role] = append(ps.RolePolicies[role], policies...) } - ps.SelfManagePolicies = append(ps.SelfManagePolicies, other.SelfManagePolicies...) + ps.IdentityScopedPolicies = append(ps.IdentityScopedPolicies, other.IdentityScopedPolicies...) return ps } @@ -62,7 +62,7 @@ func IAMPolicySet() *PolicySet { AddRolePolicy("VIEWER", IAMViewerPolicy). AddRolePolicy("EMPLOYEE", IAMViewerPolicy). AddRolePolicy("AUDITOR", IAMViewerPolicy). - AddSelfManagePolicy( + AddIdentityScopedPolicy( IAMSelfManageIdentityPolicy, IAMSelfManageSessionPolicy, IAMSelfManageInvitationPolicy, diff --git a/pkg/iam/policy_set_test.go b/pkg/iam/policy_set_test.go index de268f801..34980839a 100644 --- a/pkg/iam/policy_set_test.go +++ b/pkg/iam/policy_set_test.go @@ -28,13 +28,13 @@ func TestPolicySet_AddAndMerge(t *testing.T) { iamPolicies := NewPolicySet(). AddRolePolicy("OWNER", policy.NewPolicy("iam-owner", "IAM Owner", policy.Allow("iam:*"))). AddRolePolicy("ADMIN", policy.NewPolicy("iam-admin", "IAM Admin", policy.Allow("iam:read:*"))). - AddSelfManagePolicy(policy.NewPolicy("iam-self", "IAM Self", policy.Allow("iam:identity:get"))) + AddIdentityScopedPolicy(policy.NewPolicy("iam-self", "IAM Self", policy.Allow("iam:identity:get"))) // Create second policy set (simulating Documents service) docsPolicies := NewPolicySet(). AddRolePolicy("OWNER", policy.NewPolicy("docs-owner", "Docs Owner", policy.Allow("docs:*"))). AddRolePolicy("VIEWER", policy.NewPolicy("docs-viewer", "Docs Viewer", policy.Allow("docs:read:*"))). - AddSelfManagePolicy(policy.NewPolicy("docs-self", "Docs Self", policy.Allow("docs:own:*"))) + AddIdentityScopedPolicy(policy.NewPolicy("docs-self", "Docs Self", policy.Allow("docs:own:*"))) // Merge them combined := iamPolicies.Merge(docsPolicies) @@ -53,8 +53,8 @@ func TestPolicySet_AddAndMerge(t *testing.T) { require.Len(t, viewerPolicies, 1, "should have 1 VIEWER policy") // Test self-manage policies from both services - selfPolicies := combined.SelfManagePolicies - require.Len(t, selfPolicies, 2, "should have 2 self-manage policies") + identityPolicies := combined.IdentityScopedPolicies + require.Len(t, identityPolicies, 2, "should have 2 identity-scoped policies") } func TestIAMPolicySet(t *testing.T) { @@ -69,5 +69,5 @@ func TestIAMPolicySet(t *testing.T) { } // Should have self-manage policies - assert.NotEmpty(t, policySet.SelfManagePolicies, "expected self-manage policies") + assert.NotEmpty(t, policySet.IdentityScopedPolicies, "expected identity-scoped policies") } diff --git a/pkg/iam/service.go b/pkg/iam/service.go index c9a49dea6..2838bd9b3 100644 --- a/pkg/iam/service.go +++ b/pkg/iam/service.go @@ -247,3 +247,28 @@ func (s *Service) GetSAMLconfiguration(ctx context.Context, samlConfigurationID return samlConfiguration, nil } + +func (s *Service) GetPersonalAPIKey(ctx context.Context, personalAPIKeyID gid.GID) (*coredata.PersonalAPIKey, error) { + personalAPIKey := &coredata.PersonalAPIKey{} + + err := s.pg.WithConn( + ctx, + func(conn pg.Conn) error { + err := personalAPIKey.LoadByID(ctx, conn, personalAPIKeyID) + if err != nil { + if err == coredata.ErrResourceNotFound { + return NewPersonalAPIKeyNotFoundError(personalAPIKeyID) + } + + return fmt.Errorf("cannot load personal API key: %w", err) + } + + return nil + }, + ) + if err != nil { + return nil, err + } + + return personalAPIKey, nil +} diff --git a/pkg/probo/policies.go b/pkg/probo/policies.go index b86864cd2..6548a90d0 100644 --- a/pkg/probo/policies.go +++ b/pkg/probo/policies.go @@ -19,34 +19,32 @@ import ( "go.probo.inc/probo/pkg/iam/policy" ) +var organizationCondition = policy.Equals("principal.organization_id", "resource.organization_id") + // OwnerPolicy defines permissions for organization owners. var OwnerPolicy = policy.NewPolicy( "probo:owner", "Probo Owner", - // Full access to all probo resources - policy.Allow("core:*").WithSID("full-core-access"), + policy.Allow("core:*").WithSID("full-core-access").When(organizationCondition), ).WithDescription("Full probo access for organization owners") // AdminPolicy defines permissions for organization admins. var AdminPolicy = policy.NewPolicy( "probo:admin", "Probo Admin", - // Full access to all probo resources (same as owner for core entities) - policy.Allow("core:*").WithSID("full-c ore-access"), + policy.Allow("core:*").WithSID("full-core-access").When(organizationCondition), ).WithDescription("Probo admin access - can manage core entities") // ViewerPolicy defines read-only permissions for organization viewers. var ViewerPolicy = policy.NewPolicy( "probo:viewer", "Probo Viewer", - // Organization read actions policy.Allow( ActionOrganizationGet, ActionOrganizationGetLogoUrl, ActionOrganizationGetHorizontalLogoUrl, - ).WithSID("org-read-access"), + ).WithSID("org-read-access").When(organizationCondition), - // Entity read actions policy.Allow( ActionPeopleGet, ActionPeopleList, ActionVendorGet, ActionVendorList, @@ -79,42 +77,33 @@ var ViewerPolicy = policy.NewPolicy( ActionMeetingGet, ActionMeetingList, ActionFileGet, ActionFileDownloadUrl, ActionSlackConnectionList, - ).WithSID("entity-read-access"), + ).WithSID("entity-read-access").When(organizationCondition), - // TrustCenter read actions policy.Allow( ActionTrustCenterGet, ActionTrustCenterAccessGet, ActionTrustCenterAccessList, ActionTrustCenterDocumentAccessList, ActionTrustCenterFileGet, ActionTrustCenterFileList, ActionTrustCenterFileGetFileUrl, ActionTrustCenterReferenceList, ActionTrustCenterReferenceGetLogoUrl, - ).WithSID("trust-center-read-access"), + ).WithSID("trust-center-read-access").When(organizationCondition), - // CustomDomain read actions - policy.Allow(ActionCustomDomainGet).WithSID("custom-domain-read"), - - // OrganizationContext read actions - policy.Allow(ActionOrganizationContextGet).WithSID("organization-context-read"), - - // Document signing actions + policy.Allow(ActionCustomDomainGet).WithSID("custom-domain-read").When(organizationCondition), + policy.Allow(ActionOrganizationContextGet).WithSID("organization-context-read").When(organizationCondition), policy.Allow( ActionDocumentVersionExportPDF, ActionDocumentVersionExportSignable, ActionDocumentVersionSign, - ).WithSID("document-signing"), + ).WithSID("document-signing").When(organizationCondition), ).WithDescription("Read-only probo access for organization viewers") // AuditorPolicy defines permissions for auditor role. -// Auditors have read access to non-employee content plus some specific auditor features. var AuditorPolicy = policy.NewPolicy( "probo:auditor", "Probo Auditor", - // Same as viewer but without employee-specific content policy.Allow( ActionOrganizationGet, ActionOrganizationGetLogoUrl, ActionOrganizationGetHorizontalLogoUrl, - ).WithSID("org-read-access"), + ).WithSID("org-read-access").When(organizationCondition), - // Entity read access (same as viewer) policy.Allow( ActionPeopleGet, ActionPeopleList, ActionVendorGet, ActionVendorList, @@ -145,40 +134,34 @@ var AuditorPolicy = policy.NewPolicy( ActionSnapshotGet, ActionSnapshotList, ActionMeetingGet, ActionMeetingList, ActionFileGet, ActionFileDownloadUrl, - ).WithSID("entity-read-access"), + ).WithSID("entity-read-access").When(organizationCondition), - // Document signing actions policy.Allow( ActionDocumentVersionExportPDF, ActionDocumentVersionExportSignable, ActionDocumentVersionSign, - ).WithSID("document-signing"), + ).WithSID("document-signing").When(organizationCondition), ).WithDescription("Read-only probo access for auditors (excludes internal/employee content)") // EmployeePolicy defines permissions for employee role. -// Employees have access to internal documents and some limited read access. var EmployeePolicy = policy.NewPolicy( "probo:employee", "Probo Employee", - // Basic organization access policy.Allow( ActionOrganizationGet, ActionOrganizationGetLogoUrl, - ).WithSID("org-basic-access"), + ).WithSID("org-basic-access").When(organizationCondition), - // Document signing access policy.Allow( ActionDocumentGet, ActionDocumentList, - ).WithSID("document-signing-access"), + ).WithSID("document-signing-access").When(organizationCondition), - // Document version signing policy.Allow( ActionDocumentVersionGet, ActionDocumentVersionList, ActionDocumentVersionSign, ActionDocumentVersionExportSignable, - ).WithSID("document-version-signing"), + ).WithSID("document-version-signing").When(organizationCondition), ).WithDescription("Employee access - can sign documents and view internal content") // ProboPolicySet returns the PolicySet for the probo service. -// This is registered with the IAM Authorizer when probo.Service is created. func ProboPolicySet() *iam.PolicySet { return iam.NewPolicySet(). AddRolePolicy("OWNER", OwnerPolicy). diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql index 847da59bc..76a7d51d9 100644 --- a/pkg/server/api/connect/v1/schema.graphql +++ b/pkg/server/api/connect/v1/schema.graphql @@ -86,9 +86,6 @@ type Mutation { createPersonalAPIKey( input: CreatePersonalAPIKeyInput! ): CreatePersonalAPIKeyPayload @session(required: PRESENT) - revealPersonalAPIKeyToken( - input: RevealPersonalAPIKeyTokenInput! - ): RevealPersonalAPIKeyTokenPayload @session(required: PRESENT) revokePersonalAPIKey( input: RevokePersonalAPIKeyInput! ): RevokePersonalAPIKeyPayload @session(required: PRESENT) @@ -285,11 +282,10 @@ type Session implements Node { type PersonalAPIKey implements Node { id: ID! name: String! - lastUsedAt: Datetime expiresAt: Datetime! createdAt: Datetime! - scopes: [TokenScope!]! - organizations: [Organization!]! + + token: String @goField(forceResolver: true) permission(action: String!): Boolean! @goField(forceResolver: true) @@ -576,15 +572,10 @@ input RevokeSessionInput { input CreatePersonalAPIKeyInput { name: String! expiresAt: Datetime! - organizationIds: [ID!]! } input RevokePersonalAPIKeyInput { - tokenId: ID! -} - -input RevealPersonalAPIKeyTokenInput { - tokenId: ID! + personalAPIKeyId: ID! } input CreateOrganizationInput { @@ -751,11 +742,7 @@ type CreatePersonalAPIKeyPayload { } type RevokePersonalAPIKeyPayload { - success: Boolean! -} - -type RevealPersonalAPIKeyTokenPayload { - token: String! + personalAPIKeyId: ID! } type CreateOrganizationPayload { diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go index 65ab3dea7..70b894381 100644 --- a/pkg/server/api/connect/v1/schema/schema.go +++ b/pkg/server/api/connect/v1/schema/schema.go @@ -218,7 +218,6 @@ type ComplexityRoot struct { InviteMember func(childComplexity int, input types.InviteMemberInput) int RemoveMember func(childComplexity int, input types.RemoveMemberInput) int ResetPassword func(childComplexity int, input types.ResetPasswordInput) int - RevealPersonalAPIKeyToken func(childComplexity int, input types.RevealPersonalAPIKeyTokenInput) int RevokeAllSessions func(childComplexity int) int RevokePersonalAPIKey func(childComplexity int, input types.RevokePersonalAPIKeyInput) int RevokeSession func(childComplexity int, input types.RevokeSessionInput) int @@ -267,14 +266,12 @@ type ComplexityRoot struct { } PersonalAPIKey struct { - CreatedAt func(childComplexity int) int - ExpiresAt func(childComplexity int) int - ID func(childComplexity int) int - LastUsedAt func(childComplexity int) int - Name func(childComplexity int) int - Organizations func(childComplexity int) int - Permission func(childComplexity int, action string) int - Scopes func(childComplexity int) int + CreatedAt func(childComplexity int) int + ExpiresAt func(childComplexity int) int + ID func(childComplexity int) int + Name func(childComplexity int) int + Permission func(childComplexity int, action string) int + Token func(childComplexity int) int } PersonalAPIKeyConnection struct { @@ -302,16 +299,12 @@ type ComplexityRoot struct { Success func(childComplexity int) int } - RevealPersonalAPIKeyTokenPayload struct { - Token func(childComplexity int) int - } - RevokeAllSessionsPayload struct { RevokedCount func(childComplexity int) int } RevokePersonalAPIKeyPayload struct { - Success func(childComplexity int) int + PersonalAPIKeyID func(childComplexity int) int } RevokeSessionPayload struct { @@ -462,7 +455,6 @@ type MutationResolver interface { RevokeSession(ctx context.Context, input types.RevokeSessionInput) (*types.RevokeSessionPayload, error) RevokeAllSessions(ctx context.Context) (*types.RevokeAllSessionsPayload, error) CreatePersonalAPIKey(ctx context.Context, input types.CreatePersonalAPIKeyInput) (*types.CreatePersonalAPIKeyPayload, error) - RevealPersonalAPIKeyToken(ctx context.Context, input types.RevealPersonalAPIKeyTokenInput) (*types.RevealPersonalAPIKeyTokenPayload, error) RevokePersonalAPIKey(ctx context.Context, input types.RevokePersonalAPIKeyInput) (*types.RevokePersonalAPIKeyPayload, error) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) @@ -488,6 +480,7 @@ type OrganizationResolver interface { Permission(ctx context.Context, obj *types.Organization, action string) (bool, error) } type PersonalAPIKeyResolver interface { + Token(ctx context.Context, obj *types.PersonalAPIKey) (*string, error) Permission(ctx context.Context, obj *types.PersonalAPIKey, action string) (bool, error) } type PersonalAPIKeyConnectionResolver interface { @@ -1146,17 +1139,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.Mutation.ResetPassword(childComplexity, args["input"].(types.ResetPasswordInput)), true - case "Mutation.revealPersonalAPIKeyToken": - if e.complexity.Mutation.RevealPersonalAPIKeyToken == nil { - break - } - - args, err := ec.field_Mutation_revealPersonalAPIKeyToken_args(ctx, rawArgs) - if err != nil { - return 0, false - } - - return e.complexity.Mutation.RevealPersonalAPIKeyToken(childComplexity, args["input"].(types.RevealPersonalAPIKeyTokenInput)), true case "Mutation.revokeAllSessions": if e.complexity.Mutation.RevokeAllSessions == nil { break @@ -1443,24 +1425,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.PersonalAPIKey.ID(childComplexity), true - case "PersonalAPIKey.lastUsedAt": - if e.complexity.PersonalAPIKey.LastUsedAt == nil { - break - } - - return e.complexity.PersonalAPIKey.LastUsedAt(childComplexity), true case "PersonalAPIKey.name": if e.complexity.PersonalAPIKey.Name == nil { break } return e.complexity.PersonalAPIKey.Name(childComplexity), true - case "PersonalAPIKey.organizations": - if e.complexity.PersonalAPIKey.Organizations == nil { - break - } - - return e.complexity.PersonalAPIKey.Organizations(childComplexity), true case "PersonalAPIKey.permission": if e.complexity.PersonalAPIKey.Permission == nil { break @@ -1472,12 +1442,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin } return e.complexity.PersonalAPIKey.Permission(childComplexity, args["action"].(string)), true - case "PersonalAPIKey.scopes": - if e.complexity.PersonalAPIKey.Scopes == nil { + case "PersonalAPIKey.token": + if e.complexity.PersonalAPIKey.Token == nil { break } - return e.complexity.PersonalAPIKey.Scopes(childComplexity), true + return e.complexity.PersonalAPIKey.Token(childComplexity), true case "PersonalAPIKeyConnection.edges": if e.complexity.PersonalAPIKeyConnection.Edges == nil { @@ -1554,13 +1524,6 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.ResetPasswordPayload.Success(childComplexity), true - case "RevealPersonalAPIKeyTokenPayload.token": - if e.complexity.RevealPersonalAPIKeyTokenPayload.Token == nil { - break - } - - return e.complexity.RevealPersonalAPIKeyTokenPayload.Token(childComplexity), true - case "RevokeAllSessionsPayload.revokedCount": if e.complexity.RevokeAllSessionsPayload.RevokedCount == nil { break @@ -1568,12 +1531,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.complexity.RevokeAllSessionsPayload.RevokedCount(childComplexity), true - case "RevokePersonalAPIKeyPayload.success": - if e.complexity.RevokePersonalAPIKeyPayload.Success == nil { + case "RevokePersonalAPIKeyPayload.personalAPIKeyId": + if e.complexity.RevokePersonalAPIKeyPayload.PersonalAPIKeyID == nil { break } - return e.complexity.RevokePersonalAPIKeyPayload.Success(childComplexity), true + return e.complexity.RevokePersonalAPIKeyPayload.PersonalAPIKeyID(childComplexity), true case "RevokeSessionPayload.success": if e.complexity.RevokeSessionPayload.Success == nil { @@ -1934,7 +1897,6 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { ec.unmarshalInputMembershipOrder, ec.unmarshalInputRemoveMemberInput, ec.unmarshalInputResetPasswordInput, - ec.unmarshalInputRevealPersonalAPIKeyTokenInput, ec.unmarshalInputRevokePersonalAPIKeyInput, ec.unmarshalInputRevokeSessionInput, ec.unmarshalInputSAMLAttributeMappingsInput, @@ -2131,9 +2093,6 @@ type Mutation { createPersonalAPIKey( input: CreatePersonalAPIKeyInput! ): CreatePersonalAPIKeyPayload @session(required: PRESENT) - revealPersonalAPIKeyToken( - input: RevealPersonalAPIKeyTokenInput! - ): RevealPersonalAPIKeyTokenPayload @session(required: PRESENT) revokePersonalAPIKey( input: RevokePersonalAPIKeyInput! ): RevokePersonalAPIKeyPayload @session(required: PRESENT) @@ -2330,11 +2289,10 @@ type Session implements Node { type PersonalAPIKey implements Node { id: ID! name: String! - lastUsedAt: Datetime expiresAt: Datetime! createdAt: Datetime! - scopes: [TokenScope!]! - organizations: [Organization!]! + + token: String @goField(forceResolver: true) permission(action: String!): Boolean! @goField(forceResolver: true) @@ -2621,15 +2579,10 @@ input RevokeSessionInput { input CreatePersonalAPIKeyInput { name: String! expiresAt: Datetime! - organizationIds: [ID!]! } input RevokePersonalAPIKeyInput { - tokenId: ID! -} - -input RevealPersonalAPIKeyTokenInput { - tokenId: ID! + personalAPIKeyId: ID! } input CreateOrganizationInput { @@ -2796,11 +2749,7 @@ type CreatePersonalAPIKeyPayload { } type RevokePersonalAPIKeyPayload { - success: Boolean! -} - -type RevealPersonalAPIKeyTokenPayload { - token: String! + personalAPIKeyId: ID! } type CreateOrganizationPayload { @@ -3198,17 +3147,6 @@ func (ec *executionContext) field_Mutation_resetPassword_args(ctx context.Contex return args, nil } -func (ec *executionContext) field_Mutation_revealPersonalAPIKeyToken_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { - var err error - args := map[string]any{} - arg0, err := graphql.ProcessArgField(ctx, rawArgs, "input", ec.unmarshalNRevealPersonalAPIKeyTokenInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐRevealPersonalAPIKeyTokenInput) - if err != nil { - return nil, err - } - args["input"] = arg0 - return args, nil -} - func (ec *executionContext) field_Mutation_revokePersonalAPIKey_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -6734,69 +6672,6 @@ func (ec *executionContext) fieldContext_Mutation_createPersonalAPIKey(ctx conte return fc, nil } -func (ec *executionContext) _Mutation_revealPersonalAPIKeyToken(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - ec.fieldContext_Mutation_revealPersonalAPIKeyToken, - func(ctx context.Context) (any, error) { - fc := graphql.GetFieldContext(ctx) - return ec.resolvers.Mutation().RevealPersonalAPIKeyToken(ctx, fc.Args["input"].(types.RevealPersonalAPIKeyTokenInput)) - }, - func(ctx context.Context, next graphql.Resolver) graphql.Resolver { - directive0 := next - - directive1 := func(ctx context.Context) (any, error) { - required, err := ec.unmarshalNSessionRequirement2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐSessionRequirement(ctx, "PRESENT") - if err != nil { - var zeroVal *types.RevealPersonalAPIKeyTokenPayload - return zeroVal, err - } - if ec.directives.Session == nil { - var zeroVal *types.RevealPersonalAPIKeyTokenPayload - return zeroVal, errors.New("directive session is not implemented") - } - return ec.directives.Session(ctx, nil, directive0, required) - } - - next = directive1 - return next - }, - ec.marshalORevealPersonalAPIKeyTokenPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐRevealPersonalAPIKeyTokenPayload, - true, - false, - ) -} - -func (ec *executionContext) fieldContext_Mutation_revealPersonalAPIKeyToken(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Mutation", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "token": - return ec.fieldContext_RevealPersonalAPIKeyTokenPayload_token(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type RevealPersonalAPIKeyTokenPayload", field.Name) - }, - } - defer func() { - if r := recover(); r != nil { - err = ec.Recover(ctx, r) - ec.Error(ctx, err) - } - }() - ctx = graphql.WithFieldContext(ctx, fc) - if fc.Args, err = ec.field_Mutation_revealPersonalAPIKeyToken_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { - ec.Error(ctx, err) - return fc, err - } - return fc, nil -} - func (ec *executionContext) _Mutation_revokePersonalAPIKey(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -6840,8 +6715,8 @@ func (ec *executionContext) fieldContext_Mutation_revokePersonalAPIKey(ctx conte IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { switch field.Name { - case "success": - return ec.fieldContext_RevokePersonalAPIKeyPayload_success(ctx, field) + case "personalAPIKeyId": + return ec.fieldContext_RevokePersonalAPIKeyPayload_personalAPIKeyId(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type RevokePersonalAPIKeyPayload", field.Name) }, @@ -8440,35 +8315,6 @@ func (ec *executionContext) fieldContext_PersonalAPIKey_name(_ context.Context, return fc, nil } -func (ec *executionContext) _PersonalAPIKey_lastUsedAt(ctx context.Context, field graphql.CollectedField, obj *types.PersonalAPIKey) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - ec.fieldContext_PersonalAPIKey_lastUsedAt, - func(ctx context.Context) (any, error) { - return obj.LastUsedAt, nil - }, - nil, - ec.marshalODatetime2ᚖtimeᚐTime, - true, - false, - ) -} - -func (ec *executionContext) fieldContext_PersonalAPIKey_lastUsedAt(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "PersonalAPIKey", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Datetime does not have child fields") - }, - } - return fc, nil -} - func (ec *executionContext) _PersonalAPIKey_expiresAt(ctx context.Context, field graphql.CollectedField, obj *types.PersonalAPIKey) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -8527,91 +8373,30 @@ func (ec *executionContext) fieldContext_PersonalAPIKey_createdAt(_ context.Cont return fc, nil } -func (ec *executionContext) _PersonalAPIKey_scopes(ctx context.Context, field graphql.CollectedField, obj *types.PersonalAPIKey) (ret graphql.Marshaler) { +func (ec *executionContext) _PersonalAPIKey_token(ctx context.Context, field graphql.CollectedField, obj *types.PersonalAPIKey) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, - ec.fieldContext_PersonalAPIKey_scopes, + ec.fieldContext_PersonalAPIKey_token, func(ctx context.Context) (any, error) { - return obj.Scopes, nil + return ec.resolvers.PersonalAPIKey().Token(ctx, obj) }, nil, - ec.marshalNTokenScope2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐTokenScopeᚄ, - true, + ec.marshalOString2ᚖstring, true, + false, ) } -func (ec *executionContext) fieldContext_PersonalAPIKey_scopes(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_PersonalAPIKey_token(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "PersonalAPIKey", Field: field, - IsMethod: false, - IsResolver: false, + IsMethod: true, + IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type TokenScope does not have child fields") - }, - } - return fc, nil -} - -func (ec *executionContext) _PersonalAPIKey_organizations(ctx context.Context, field graphql.CollectedField, obj *types.PersonalAPIKey) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - ec.fieldContext_PersonalAPIKey_organizations, - func(ctx context.Context) (any, error) { - return obj.Organizations, nil - }, - nil, - ec.marshalNOrganization2ᚕᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐOrganizationᚄ, - true, - true, - ) -} - -func (ec *executionContext) fieldContext_PersonalAPIKey_organizations(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "PersonalAPIKey", - Field: field, - IsMethod: false, - IsResolver: false, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - switch field.Name { - case "id": - return ec.fieldContext_Organization_id(ctx, field) - case "name": - return ec.fieldContext_Organization_name(ctx, field) - case "logoUrl": - return ec.fieldContext_Organization_logoUrl(ctx, field) - case "horizontalLogoUrl": - return ec.fieldContext_Organization_horizontalLogoUrl(ctx, field) - case "email": - return ec.fieldContext_Organization_email(ctx, field) - case "description": - return ec.fieldContext_Organization_description(ctx, field) - case "websiteUrl": - return ec.fieldContext_Organization_websiteUrl(ctx, field) - case "headquarterAddress": - return ec.fieldContext_Organization_headquarterAddress(ctx, field) - case "createdAt": - return ec.fieldContext_Organization_createdAt(ctx, field) - case "updatedAt": - return ec.fieldContext_Organization_updatedAt(ctx, field) - case "members": - return ec.fieldContext_Organization_members(ctx, field) - case "invitations": - return ec.fieldContext_Organization_invitations(ctx, field) - case "samlConfigurations": - return ec.fieldContext_Organization_samlConfigurations(ctx, field) - case "viewerMembership": - return ec.fieldContext_Organization_viewerMembership(ctx, field) - case "permission": - return ec.fieldContext_Organization_permission(ctx, field) - } - return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name) + return nil, errors.New("field of type String does not have child fields") }, } return fc, nil @@ -8807,16 +8592,12 @@ func (ec *executionContext) fieldContext_PersonalAPIKeyEdge_node(_ context.Conte return ec.fieldContext_PersonalAPIKey_id(ctx, field) case "name": return ec.fieldContext_PersonalAPIKey_name(ctx, field) - case "lastUsedAt": - return ec.fieldContext_PersonalAPIKey_lastUsedAt(ctx, field) case "expiresAt": return ec.fieldContext_PersonalAPIKey_expiresAt(ctx, field) case "createdAt": return ec.fieldContext_PersonalAPIKey_createdAt(ctx, field) - case "scopes": - return ec.fieldContext_PersonalAPIKey_scopes(ctx, field) - case "organizations": - return ec.fieldContext_PersonalAPIKey_organizations(ctx, field) + case "token": + return ec.fieldContext_PersonalAPIKey_token(ctx, field) case "permission": return ec.fieldContext_PersonalAPIKey_permission(ctx, field) } @@ -9218,35 +8999,6 @@ func (ec *executionContext) fieldContext_ResetPasswordPayload_success(_ context. return fc, nil } -func (ec *executionContext) _RevealPersonalAPIKeyTokenPayload_token(ctx context.Context, field graphql.CollectedField, obj *types.RevealPersonalAPIKeyTokenPayload) (ret graphql.Marshaler) { - return graphql.ResolveField( - ctx, - ec.OperationContext, - field, - ec.fieldContext_RevealPersonalAPIKeyTokenPayload_token, - func(ctx context.Context) (any, error) { - return obj.Token, nil - }, - nil, - ec.marshalNString2string, - true, - true, - ) -} - -func (ec *executionContext) fieldContext_RevealPersonalAPIKeyTokenPayload_token(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "RevealPersonalAPIKeyTokenPayload", - 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) _RevokeAllSessionsPayload_revokedCount(ctx context.Context, field graphql.CollectedField, obj *types.RevokeAllSessionsPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -9276,30 +9028,30 @@ func (ec *executionContext) fieldContext_RevokeAllSessionsPayload_revokedCount(_ return fc, nil } -func (ec *executionContext) _RevokePersonalAPIKeyPayload_success(ctx context.Context, field graphql.CollectedField, obj *types.RevokePersonalAPIKeyPayload) (ret graphql.Marshaler) { +func (ec *executionContext) _RevokePersonalAPIKeyPayload_personalAPIKeyId(ctx context.Context, field graphql.CollectedField, obj *types.RevokePersonalAPIKeyPayload) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, ec.OperationContext, field, - ec.fieldContext_RevokePersonalAPIKeyPayload_success, + ec.fieldContext_RevokePersonalAPIKeyPayload_personalAPIKeyId, func(ctx context.Context) (any, error) { - return obj.Success, nil + return obj.PersonalAPIKeyID, nil }, nil, - ec.marshalNBoolean2bool, + ec.marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID, true, true, ) } -func (ec *executionContext) fieldContext_RevokePersonalAPIKeyPayload_success(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { +func (ec *executionContext) fieldContext_RevokePersonalAPIKeyPayload_personalAPIKeyId(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { fc = &graphql.FieldContext{ Object: "RevokePersonalAPIKeyPayload", Field: field, IsMethod: false, IsResolver: false, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + return nil, errors.New("field of type ID does not have child fields") }, } return fc, nil @@ -12757,7 +12509,7 @@ func (ec *executionContext) unmarshalInputCreatePersonalAPIKeyInput(ctx context. asMap[k] = v } - fieldsInOrder := [...]string{"name", "expiresAt", "organizationIds"} + fieldsInOrder := [...]string{"name", "expiresAt"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -12778,13 +12530,6 @@ func (ec *executionContext) unmarshalInputCreatePersonalAPIKeyInput(ctx context. return it, err } it.ExpiresAt = data - case "organizationIds": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("organizationIds")) - data, err := ec.unmarshalNID2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx, v) - if err != nil { - return it, err - } - it.OrganizationIds = data } } @@ -13200,33 +12945,6 @@ func (ec *executionContext) unmarshalInputResetPasswordInput(ctx context.Context return it, nil } -func (ec *executionContext) unmarshalInputRevealPersonalAPIKeyTokenInput(ctx context.Context, obj any) (types.RevealPersonalAPIKeyTokenInput, error) { - var it types.RevealPersonalAPIKeyTokenInput - asMap := map[string]any{} - for k, v := range obj.(map[string]any) { - asMap[k] = v - } - - fieldsInOrder := [...]string{"tokenId"} - for _, k := range fieldsInOrder { - v, ok := asMap[k] - if !ok { - continue - } - switch k { - case "tokenId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("tokenId")) - data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) - if err != nil { - return it, err - } - it.TokenID = data - } - } - - return it, nil -} - func (ec *executionContext) unmarshalInputRevokePersonalAPIKeyInput(ctx context.Context, obj any) (types.RevokePersonalAPIKeyInput, error) { var it types.RevokePersonalAPIKeyInput asMap := map[string]any{} @@ -13234,20 +12952,20 @@ func (ec *executionContext) unmarshalInputRevokePersonalAPIKeyInput(ctx context. asMap[k] = v } - fieldsInOrder := [...]string{"tokenId"} + fieldsInOrder := [...]string{"personalAPIKeyId"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { continue } switch k { - case "tokenId": - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("tokenId")) + case "personalAPIKeyId": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("personalAPIKeyId")) data, err := ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, v) if err != nil { return it, err } - it.TokenID = data + it.PersonalAPIKeyID = data } } @@ -15397,10 +15115,6 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_createPersonalAPIKey(ctx, field) }) - case "revealPersonalAPIKeyToken": - out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { - return ec._Mutation_revealPersonalAPIKeyToken(ctx, field) - }) case "revokePersonalAPIKey": out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { return ec._Mutation_revokePersonalAPIKey(ctx, field) @@ -15927,8 +15641,6 @@ func (ec *executionContext) _PersonalAPIKey(ctx context.Context, sel ast.Selecti if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } - case "lastUsedAt": - out.Values[i] = ec._PersonalAPIKey_lastUsedAt(ctx, field, obj) case "expiresAt": out.Values[i] = ec._PersonalAPIKey_expiresAt(ctx, field, obj) if out.Values[i] == graphql.Null { @@ -15939,16 +15651,39 @@ func (ec *executionContext) _PersonalAPIKey(ctx context.Context, sel ast.Selecti if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } - case "scopes": - out.Values[i] = ec._PersonalAPIKey_scopes(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + case "token": + field := field + + innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._PersonalAPIKey_token(ctx, field, obj) + return res } - case "organizations": - out.Values[i] = ec._PersonalAPIKey_organizations(ctx, field, obj) - if out.Values[i] == graphql.Null { - atomic.AddUint32(&out.Invalids, 1) + + 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 "permission": field := field @@ -16317,45 +16052,6 @@ func (ec *executionContext) _ResetPasswordPayload(ctx context.Context, sel ast.S return out } -var revealPersonalAPIKeyTokenPayloadImplementors = []string{"RevealPersonalAPIKeyTokenPayload"} - -func (ec *executionContext) _RevealPersonalAPIKeyTokenPayload(ctx context.Context, sel ast.SelectionSet, obj *types.RevealPersonalAPIKeyTokenPayload) graphql.Marshaler { - fields := graphql.CollectFields(ec.OperationContext, sel, revealPersonalAPIKeyTokenPayloadImplementors) - - out := graphql.NewFieldSet(fields) - deferred := make(map[string]*graphql.FieldSet) - for i, field := range fields { - switch field.Name { - case "__typename": - out.Values[i] = graphql.MarshalString("RevealPersonalAPIKeyTokenPayload") - case "token": - out.Values[i] = ec._RevealPersonalAPIKeyTokenPayload_token(ctx, field, obj) - if out.Values[i] == graphql.Null { - out.Invalids++ - } - default: - panic("unknown field " + strconv.Quote(field.Name)) - } - } - out.Dispatch(ctx) - if out.Invalids > 0 { - return graphql.Null - } - - atomic.AddInt32(&ec.deferred, int32(len(deferred))) - - for label, dfs := range deferred { - ec.processDeferredGroup(graphql.DeferredGroup{ - Label: label, - Path: graphql.GetPath(ctx), - FieldSet: dfs, - Context: ctx, - }) - } - - return out -} - var revokeAllSessionsPayloadImplementors = []string{"RevokeAllSessionsPayload"} func (ec *executionContext) _RevokeAllSessionsPayload(ctx context.Context, sel ast.SelectionSet, obj *types.RevokeAllSessionsPayload) graphql.Marshaler { @@ -16406,8 +16102,8 @@ func (ec *executionContext) _RevokePersonalAPIKeyPayload(ctx context.Context, se switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("RevokePersonalAPIKeyPayload") - case "success": - out.Values[i] = ec._RevokePersonalAPIKeyPayload_success(ctx, field, obj) + case "personalAPIKeyId": + out.Values[i] = ec._RevokePersonalAPIKeyPayload_personalAPIKeyId(ctx, field, obj) if out.Values[i] == graphql.Null { out.Invalids++ } @@ -18012,36 +17708,6 @@ func (ec *executionContext) marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGI return res } -func (ec *executionContext) unmarshalNID2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx context.Context, v any) ([]gid.GID, error) { - var vSlice []any - vSlice = graphql.CoerceList(v) - var err error - res := make([]gid.GID, len(vSlice)) - for i := range vSlice { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) - res[i], err = ec.unmarshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, vSlice[i]) - if err != nil { - return nil, err - } - } - return res, nil -} - -func (ec *executionContext) marshalNID2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋgidᚐGIDᚄ(ctx context.Context, sel ast.SelectionSet, v []gid.GID) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - for i := range v { - ret[i] = ec.marshalNID2goᚗproboᚗincᚋproboᚋpkgᚋgidᚐGID(ctx, sel, v[i]) - } - - for _, e := range ret { - if e == graphql.Null { - return graphql.Null - } - } - - return ret -} - func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v any) (int, error) { res, err := graphql.UnmarshalInt(v) return res, graphql.ErrorOnPath(ctx, err) @@ -18345,50 +18011,6 @@ var ( } ) -func (ec *executionContext) marshalNOrganization2ᚕᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐOrganizationᚄ(ctx context.Context, sel ast.SelectionSet, v []*types.Organization) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalNOrganization2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐOrganization(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() - - for _, e := range ret { - if e == graphql.Null { - return graphql.Null - } - } - - return ret -} - func (ec *executionContext) marshalNOrganization2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐOrganization(ctx context.Context, sel ast.SelectionSet, v *types.Organization) graphql.Marshaler { if v == nil { if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { @@ -18487,11 +18109,6 @@ func (ec *executionContext) unmarshalNResetPasswordInput2goᚗproboᚗincᚋprob return res, graphql.ErrorOnPath(ctx, err) } -func (ec *executionContext) unmarshalNRevealPersonalAPIKeyTokenInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐRevealPersonalAPIKeyTokenInput(ctx context.Context, v any) (types.RevealPersonalAPIKeyTokenInput, error) { - res, err := ec.unmarshalInputRevealPersonalAPIKeyTokenInput(ctx, v) - return res, graphql.ErrorOnPath(ctx, err) -} - func (ec *executionContext) unmarshalNRevokePersonalAPIKeyInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐRevokePersonalAPIKeyInput(ctx context.Context, v any) (types.RevokePersonalAPIKeyInput, error) { res, err := ec.unmarshalInputRevokePersonalAPIKeyInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -18769,75 +18386,6 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S return res } -func (ec *executionContext) unmarshalNTokenScope2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐTokenScope(ctx context.Context, v any) (types.TokenScope, error) { - var res types.TokenScope - err := res.UnmarshalGQL(v) - return res, graphql.ErrorOnPath(ctx, err) -} - -func (ec *executionContext) marshalNTokenScope2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐTokenScope(ctx context.Context, sel ast.SelectionSet, v types.TokenScope) graphql.Marshaler { - return v -} - -func (ec *executionContext) unmarshalNTokenScope2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐTokenScopeᚄ(ctx context.Context, v any) ([]types.TokenScope, error) { - var vSlice []any - vSlice = graphql.CoerceList(v) - var err error - res := make([]types.TokenScope, len(vSlice)) - for i := range vSlice { - ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) - res[i], err = ec.unmarshalNTokenScope2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐTokenScope(ctx, vSlice[i]) - if err != nil { - return nil, err - } - } - return res, nil -} - -func (ec *executionContext) marshalNTokenScope2ᚕgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐTokenScopeᚄ(ctx context.Context, sel ast.SelectionSet, v []types.TokenScope) graphql.Marshaler { - ret := make(graphql.Array, len(v)) - var wg sync.WaitGroup - isLen1 := len(v) == 1 - if !isLen1 { - wg.Add(len(v)) - } - for i := range v { - i := i - fc := &graphql.FieldContext{ - Index: &i, - Result: &v[i], - } - ctx := graphql.WithFieldContext(ctx, fc) - f := func(i int) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = nil - } - }() - if !isLen1 { - defer wg.Done() - } - ret[i] = ec.marshalNTokenScope2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐTokenScope(ctx, sel, v[i]) - } - if isLen1 { - f(i) - } else { - go f(i) - } - - } - wg.Wait() - - for _, e := range ret { - if e == graphql.Null { - return graphql.Null - } - } - - return ret -} - func (ec *executionContext) unmarshalNUpdateMembershipInput2goᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐUpdateMembershipInput(ctx context.Context, v any) (types.UpdateMembershipInput, error) { res, err := ec.unmarshalInputUpdateMembershipInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) @@ -19436,13 +18984,6 @@ func (ec *executionContext) marshalOResetPasswordPayload2ᚖgoᚗproboᚗincᚋp return ec._ResetPasswordPayload(ctx, sel, v) } -func (ec *executionContext) marshalORevealPersonalAPIKeyTokenPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐRevealPersonalAPIKeyTokenPayload(ctx context.Context, sel ast.SelectionSet, v *types.RevealPersonalAPIKeyTokenPayload) graphql.Marshaler { - if v == nil { - return graphql.Null - } - return ec._RevealPersonalAPIKeyTokenPayload(ctx, sel, v) -} - func (ec *executionContext) marshalORevokeAllSessionsPayload2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐRevokeAllSessionsPayload(ctx context.Context, sel ast.SelectionSet, v *types.RevokeAllSessionsPayload) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/pkg/server/api/connect/v1/types/types.go b/pkg/server/api/connect/v1/types/types.go index ee636a4aa..026e41e3e 100644 --- a/pkg/server/api/connect/v1/types/types.go +++ b/pkg/server/api/connect/v1/types/types.go @@ -78,9 +78,8 @@ type CreateOrganizationPayload struct { } type CreatePersonalAPIKeyInput struct { - Name string `json:"name"` - ExpiresAt time.Time `json:"expiresAt"` - OrganizationIds []gid.GID `json:"organizationIds"` + Name string `json:"name"` + ExpiresAt time.Time `json:"expiresAt"` } type CreatePersonalAPIKeyPayload struct { @@ -273,14 +272,12 @@ type PasswordRequired struct { func (PasswordRequired) IsAssumeOrganizationSessionResult() {} type PersonalAPIKey struct { - ID gid.GID `json:"id"` - Name string `json:"name"` - LastUsedAt *time.Time `json:"lastUsedAt,omitempty"` - ExpiresAt time.Time `json:"expiresAt"` - CreatedAt time.Time `json:"createdAt"` - Scopes []TokenScope `json:"scopes"` - Organizations []*Organization `json:"organizations"` - Permission bool `json:"permission"` + ID gid.GID `json:"id"` + Name string `json:"name"` + ExpiresAt time.Time `json:"expiresAt"` + CreatedAt time.Time `json:"createdAt"` + Token *string `json:"token,omitempty"` + Permission bool `json:"permission"` } func (PersonalAPIKey) IsNode() {} @@ -312,24 +309,16 @@ type ResetPasswordPayload struct { Success bool `json:"success"` } -type RevealPersonalAPIKeyTokenInput struct { - TokenID gid.GID `json:"tokenId"` -} - -type RevealPersonalAPIKeyTokenPayload struct { - Token string `json:"token"` -} - type RevokeAllSessionsPayload struct { RevokedCount int `json:"revokedCount"` } type RevokePersonalAPIKeyInput struct { - TokenID gid.GID `json:"tokenId"` + PersonalAPIKeyID gid.GID `json:"personalAPIKeyId"` } type RevokePersonalAPIKeyPayload struct { - Success bool `json:"success"` + PersonalAPIKeyID gid.GID `json:"personalAPIKeyId"` } type RevokeSessionInput struct { diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go index d9ccf865f..cba3c9aff 100644 --- a/pkg/server/api/connect/v1/v1_resolver.go +++ b/pkg/server/api/connect/v1/v1_resolver.go @@ -677,30 +677,17 @@ func (r *mutationResolver) CreatePersonalAPIKey(ctx context.Context, input types }, nil } -// RevealPersonalAPIKeyToken is the resolver for the revealPersonalAPIKeyToken field. -func (r *mutationResolver) RevealPersonalAPIKeyToken(ctx context.Context, input types.RevealPersonalAPIKeyTokenInput) (*types.RevealPersonalAPIKeyTokenPayload, error) { - identity := IdentityFromContext(ctx) - - token, err := r.iam.AccountService.RevealPersonalAPIKeyToken(ctx, identity.ID, input.TokenID) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot reveal personal api key token", log.Error(err)) - return nil, gqlutils.InternalServerError(ctx) - } - - return &types.RevealPersonalAPIKeyTokenPayload{Token: token}, nil -} - // RevokePersonalAPIKey is the resolver for the revokePersonalAPIKey field. func (r *mutationResolver) RevokePersonalAPIKey(ctx context.Context, input types.RevokePersonalAPIKeyInput) (*types.RevokePersonalAPIKeyPayload, error) { identity := IdentityFromContext(ctx) - err := r.iam.AccountService.DeletePersonalAPIKey(ctx, identity.ID, input.TokenID) + err := r.iam.AccountService.DeletePersonalAPIKey(ctx, identity.ID, input.PersonalAPIKeyID) if err != nil { r.logger.ErrorCtx(ctx, "cannot delete personal api key", log.Error(err)) return nil, gqlutils.InternalServerError(ctx) } - return &types.RevokePersonalAPIKeyPayload{Success: true}, nil + return &types.RevokePersonalAPIKeyPayload{PersonalAPIKeyID: input.PersonalAPIKeyID}, nil } // CreateOrganization is the resolver for the createOrganization field. @@ -1012,6 +999,15 @@ func (r *organizationResolver) HorizontalLogoURL(ctx context.Context, obj *types // Members is the resolver for the members field. func (r *organizationResolver) Members(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) { + if err := r.iam.Authorizer.Authorize(ctx, iam.AuthorizeParams{ + Principal: IdentityFromContext(ctx).ID, + Resource: obj.ID, + Action: iam.ActionIAMOrganizationListMembers, + ResourceAttributes: map[string]string{}, + }); err != nil { + return nil, gqlutils.Forbidden(err) + } + if gqlutils.OnlyTotalCountSelected(ctx) { return &types.MembershipConnection{ Resolver: r, @@ -1112,6 +1108,19 @@ func (r *organizationResolver) Permission(ctx context.Context, obj *types.Organi return r.Resolver.Permission(ctx, obj, action) } +// Token is the resolver for the token field. +func (r *personalAPIKeyResolver) Token(ctx context.Context, obj *types.PersonalAPIKey) (*string, error) { + identity := IdentityFromContext(ctx) + + token, err := r.iam.AccountService.RevealPersonalAPIKeyToken(ctx, identity.ID, obj.ID) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot reveal personal api key token", log.Error(err)) + return nil, gqlutils.InternalServerError(ctx) + } + + return &token, nil +} + // Permission is the resolver for the permission field. func (r *personalAPIKeyResolver) Permission(ctx context.Context, obj *types.PersonalAPIKey, action string) (bool, error) { return r.Resolver.Permission(ctx, obj, action) @@ -1202,6 +1211,15 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error return types.NewSAMLConfiguration(samlConfiguration), nil } + case coredata.PersonalAPIKeyEntityType: + action = iam.ActionIAMPersonalAPIKeyGet + loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) { + personalAPIKey, err := r.iam.GetPersonalAPIKey(ctx, id) + if err != nil { + return nil, err + } + return types.NewPersonalAPIKey(personalAPIKey), nil + } default: return nil, fmt.Errorf("unsupported entity type: %d", id.EntityType()) }