Refactor policies document

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-01-02 11:04:48 +01:00
parent cbf338fd14
commit 4013b00841
75 changed files with 1958 additions and 1230 deletions

View File

@@ -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"
}
};
})();

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<875eae6f5455a17a1bb8e0590c7acdb0>>
* @generated SignedSource<<faeeb8835f77a5251a0311dbac21bfde>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -12,7 +12,6 @@ import { ConcreteRequest } from 'relay-runtime';
export type CreatePersonalAPIKeyInput = {
expiresAt: any;
name: string;
organizationIds: ReadonlyArray<string>;
};
export type PersonalAPIKeyListCreateMutation$variables = {
connections: ReadonlyArray<string>;
@@ -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;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<e518a98137eb07e37499d945991b15ed>>
* @generated SignedSource<<bdb7bb502fae96e1eb5b8ae644f8bebf>>
* @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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,101 @@
/**
* @generated SignedSource<<ed51014f8f35d76faaa63c10998610be>>
* @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;

View File

@@ -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;

View File

@@ -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<string>;
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;

View File

@@ -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<PersonalAPIKeyListCreateMutation>(createMutation);
const [revokeCommit] =
useMutation<PersonalAPIKeyListRevokeMutation>(revokeMutation);
const [revealTokenCommit, isRevealingToken] =
useMutation<PersonalAPIKeyListRevealTokenMutation>(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<void>((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 (
<>
<div className="space-y-4">
@@ -314,7 +170,7 @@ export function PersonalAPIKeyList(props: {
</Button>
</div>
{keys.length === 0 ? (
{viewer.personalAPIKeys.edges.length === 0 ? (
<Card padded>
<div className="text-center py-12">
<h3 className="text-lg font-medium text-gray-900 mb-2">
@@ -328,10 +184,8 @@ export function PersonalAPIKeyList(props: {
) : (
<Card padded>
<PersonalAPIKeysTable
keys={keys}
onRevoke={handleRevoke}
onShowToken={handleShowToken}
isShowingToken={isRevealingToken}
edges={viewer.personalAPIKeys.edges}
connectionId={connectionID}
/>
</Card>
)}

View File

@@ -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<PersonalAPIKeyRow_revokeMutation>(revokeMutation);
const handleRevoke = () => {
confirm(
async () => {
await new Promise<void>((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 (
<Tr className={clsx(isRevoking && "opacity-60 pointer-events-none")}>
<Td>
<div className="font-medium text-txt-primary">{key.name}</div>
<div className="text-xs text-txt-tertiary">
{expired ? __("Expired") : __("Active")}
</div>
</Td>
<Td>
<span className="text-sm text-txt-secondary">
{formatDate(key.createdAt)}
</span>
</Td>
<Td>
<span className="text-sm text-txt-secondary">
{formatDate(key.expiresAt)}
</span>
</Td>
<Td width={140} className="text-end">
<div className="flex gap-2 justify-end">
<Suspense fallback={<Spinner />}>
<PersonalAPIKeyTokenAction fKey={fKey} disabled={isRevoking} />
</Suspense>
<Button variant="danger" onClick={handleRevoke} disabled={isRevoking}>
{__("Revoke")}
</Button>
</div>
</Td>
</Tr>
);
}

View File

@@ -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 (
<>
<Button variant="secondary" onClick={handleShow} disabled={!!disabled}>
{__("Show")}
</Button>
<PersonalAPIKeyTokenDialog
dialogRef={dialogRef}
token={data.token ?? ""}
onDone={() => {
dialogRef.current?.close();
}}
/>
</>
);
}

View File

@@ -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 (
<Dialog
ref={dialogRef}

View File

@@ -1,24 +1,14 @@
import { useTranslate } from "@probo/i18n";
import { formatDate } from "@probo/helpers";
import { Button, Table, Tbody, Td, Th, Thead, Tr } from "@probo/ui";
export type PersonalAPIKeyRow = {
id: string;
name: string;
createdAt: string;
expiresAt: string;
lastUsedAt: string | null;
};
import { Table, Tbody, Th, Thead, Tr } from "@probo/ui";
import { PersonalAPIKeyRow } from "./PersonalAPIKeyRow";
import type { PersonalAPIKeyListFragment$data } from "/__generated__/iam/PersonalAPIKeyListFragment.graphql";
export function PersonalAPIKeysTable(props: {
keys: PersonalAPIKeyRow[];
onRevoke: (key: { id: string; name: string }) => 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 (
<Table>
@@ -32,51 +22,13 @@ export function PersonalAPIKeysTable(props: {
</Tr>
</Thead>
<Tbody>
{keys.map((k) => {
const expired = new Date(k.expiresAt) < now;
return (
<Tr key={k.id}>
<Td>
<div className="font-medium text-txt-primary">{k.name}</div>
<div className="text-xs text-txt-tertiary">
{expired ? __("Expired") : __("Active")}
</div>
</Td>
<Td>
<span className="text-sm text-txt-secondary">
{k.lastUsedAt ? formatDate(k.lastUsedAt) : "—"}
</span>
</Td>
<Td>
<span className="text-sm text-txt-secondary">
{formatDate(k.createdAt)}
</span>
</Td>
<Td>
<span className="text-sm text-txt-secondary">
{formatDate(k.expiresAt)}
</span>
</Td>
<Td width={140} className="text-end">
<div className="flex gap-2 justify-end">
<Button
variant="secondary"
onClick={() => onShowToken({ id: k.id, name: k.name })}
disabled={!!isShowingToken}
>
{__("Show")}
</Button>
<Button
variant="danger"
onClick={() => onRevoke({ id: k.id, name: k.name })}
>
{__("Revoke")}
</Button>
</div>
</Td>
</Tr>
);
})}
{edges.map(({ node }) => (
<PersonalAPIKeyRow
key={node.id}
fKey={node}
connectionId={connectionId}
/>
))}
</Tbody>
</Table>
);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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:

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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())

View File

@@ -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,

View File

@@ -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:

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 ""
}

View File

@@ -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")

View File

@@ -131,4 +131,3 @@ func ForEntityType(entityType uint16) ResourcePattern {
EntityType: &entityType,
}
}

View File

@@ -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
}
}

View File

@@ -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,
},

View File

@@ -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,

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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).

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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())
}